"""Tests for agent.subscription_view — the surface-agnostic /subscription core.

Behavior contracts (not change-detectors): the manage-URL builder's shape, the
payload parser's field mapping + fail-open posture, and the dev-fixture states
that drive the CLI/TUI without a live portal.
"""

from decimal import Decimal

import pytest

from agent.subscription_view import (
    CurrentSubscription,
    SubscriptionState,
    SubscriptionTier,
    build_subscription_state,
    dev_fixture_subscription_state,
    format_tier_row,
    is_upgrade,
    selectable_tiers,
    subscription_change_preview_from_payload,
    subscription_manage_url,
    subscription_state_from_payload,
)


def _tier(tid, order, dpm, mc, *, is_current=False, is_enabled=True):
    return SubscriptionTier(
        tier_id=tid,
        name=tid.title(),
        tier_order=order,
        dollars_per_month=Decimal(dpm) if dpm is not None else None,
        monthly_credits=Decimal(mc) if mc is not None else None,
        is_current=is_current,
        is_enabled=is_enabled,
    )


# ── subscription_manage_url ──────────────────────────────────────────


def test_manage_url_attaches_org_and_path_to_portal_origin():
    s = SubscriptionState(
        logged_in=True,
        org_id="org_x",
        portal_url="https://portal.nousresearch.com/billing/whatever",
    )
    # Path is replaced with /manage-subscription; org_id is pinned; origin kept.
    assert (
        subscription_manage_url(s)
        == "https://portal.nousresearch.com/manage-subscription?org_id=org_x"
    )


def test_manage_url_omits_org_when_absent():
    s = SubscriptionState(logged_in=True, org_id=None, portal_url="https://p.example.com/")
    url = subscription_manage_url(s)
    assert url == "https://p.example.com/manage-subscription"
    assert "org_id" not in url


def test_manage_url_appends_plan_when_tier_picked():
    # A picked tier rides along as ?plan=<tier_id>, org_id first, plan second.
    s = SubscriptionState(
        logged_in=True,
        org_id="org_x",
        portal_url="https://portal.nousresearch.com/billing",
    )
    assert (
        subscription_manage_url(s, tier_id="plus")
        == "https://portal.nousresearch.com/manage-subscription?org_id=org_x&plan=plus"
    )


def test_manage_url_plan_without_org():
    # No org_id → plan is still appended (the portal validates/ignores it).
    s = SubscriptionState(logged_in=True, org_id=None, portal_url="https://p.example.com/")
    assert (
        subscription_manage_url(s, tier_id="ultra")
        == "https://p.example.com/manage-subscription?plan=ultra"
    )


def test_manage_url_no_plan_when_tier_absent():
    # No tier picked → no plan= param (unchanged legacy shape).
    s = SubscriptionState(logged_in=True, org_id="org_x", portal_url="https://p.example.com/")
    url = subscription_manage_url(s)
    assert url == "https://p.example.com/manage-subscription?org_id=org_x"
    assert "plan=" not in url


def test_manage_url_preserves_unrelated_query_params():
    # Pre-existing portal query params survive; contract-owned org_id/plan are
    # appended after them, org_id before plan.
    s = SubscriptionState(
        logged_in=True, org_id="org_x", portal_url="https://portal.example/billing?ref=abc"
    )
    assert (
        subscription_manage_url(s, tier_id="plus")
        == "https://portal.example/manage-subscription?ref=abc&org_id=org_x&plan=plus"
    )


def test_manage_url_overwrites_stale_contract_params():
    # A portal_url that already carries org_id/plan gets them replaced, not doubled.
    s = SubscriptionState(
        logged_in=True, org_id="org_x", portal_url="https://portal.example/x?org_id=old&plan=stale"
    )
    assert (
        subscription_manage_url(s, tier_id="plus")
        == "https://portal.example/manage-subscription?org_id=org_x&plan=plus"
    )


def test_manage_url_rejects_non_http_scheme():
    # Only http(s) is handed to a browser open.
    assert (
        subscription_manage_url(
            SubscriptionState(logged_in=True, org_id="o", portal_url="ftp://portal.example/billing")
        )
        is None
    )
    assert (
        subscription_manage_url(
            SubscriptionState(logged_in=True, portal_url="file:///etc/passwd")
        )
        is None
    )


def test_manage_url_none_without_portal():
    assert subscription_manage_url(SubscriptionState(logged_in=True, portal_url=None)) is None


def test_manage_url_none_for_garbage_portal():
    # No scheme/netloc → can't build a deep-link; fail closed (None), not crash.
    assert subscription_manage_url(SubscriptionState(logged_in=True, portal_url="not a url")) is None


# ── shared plan-catalog helpers (format_tier_row / selectable_tiers / is_upgrade) ──


def test_format_tier_row_groups_thousands():
    # $1000+ renders thousands-grouped ($1,000), matching the TUI's toLocaleString.
    assert format_tier_row(_tier("max", 4, "1000", "3000")) == "Max · $1,000/mo · $3,000 credits/mo"


def test_format_tier_row_hides_absent_or_zero_credits():
    # A None / zero-credits tier hides the suffix — never "· — credits/mo" / "· $0 credits/mo".
    assert format_tier_row(_tier("plus", 1, "20", "0")) == "Plus · $20/mo"
    assert format_tier_row(_tier("plus", 1, "20", None)) == "Plus · $20/mo"
    assert "credits/mo" not in format_tier_row(_tier("plus", 1, "20", "0"))


def test_selectable_tiers_excludes_free_current_and_disabled():
    state = SubscriptionState(
        logged_in=True,
        tiers=(
            _tier("free", 0, "0", "0"),
            _tier("plus", 1, "20", "22"),
            _tier("legacy", 2, "40", "50", is_enabled=False),
            _tier("ultra", 3, "200", "220", is_current=True),
        ),
    )
    # free (order 0), disabled (legacy), and the current tier (ultra) are excluded.
    assert [t.tier_id for t in selectable_tiers(state)] == ["plus"]


def test_is_upgrade_by_tier_order():
    state = SubscriptionState(
        logged_in=True,
        current=CurrentSubscription(tier_id="plus", tier_name="Plus"),
        tiers=(_tier("plus", 1, "20", "22"), _tier("ultra", 3, "200", "220")),
    )
    assert is_upgrade(state, "ultra") is True
    assert is_upgrade(state, "plus") is False


def test_is_upgrade_falls_back_to_is_current_marker():
    # No explicit current subscription → derive the current order from tiers[] is_current.
    state = SubscriptionState(
        logged_in=True,
        current=None,
        tiers=(_tier("plus", 1, "20", "22", is_current=True), _tier("ultra", 3, "200", "220")),
    )
    assert is_upgrade(state, "ultra") is True


# ── payload parser ───────────────────────────────────────────────────


def test_parser_maps_camelCase_payload_fields():
    payload = {
        "org": {"name": "Acme", "id": "org_1", "role": "ADMIN"},
        "context": "personal",
        "current": {
            "tierId": "plus",
            "tierName": "Plus",
            "monthlyCredits": "1000",
            "creditsRemaining": "420",
            "cycleEndsAt": "2026-07-01",
            "cancelAtPeriodEnd": True,
            "cancellationEffectiveAt": "2026-07-01",
        },
    }
    s = subscription_state_from_payload(payload, portal_url="https://p/billing")

    assert s.logged_in is True
    assert s.org_name == "Acme" and s.org_id == "org_1"
    assert s.is_admin is True and s.can_change_plan is True
    assert s.current is not None
    assert s.current.tier_name == "Plus"
    assert s.current.cancel_at_period_end is True
    assert s.current.monthly_credits == Decimal("1000")


def test_parser_no_plan_is_none_not_all_null_object():
    # "No plan" is current:null on the wire; a current-shaped dict with no
    # tierId must parse to None (not an all-null CurrentSubscription).
    s = subscription_state_from_payload({"current": {"tierId": None}}, portal_url=None)
    assert s.current is None


def test_parser_member_role_cannot_change_plan():
    s = subscription_state_from_payload({"org": {"role": "MEMBER"}}, portal_url=None)
    assert s.is_admin is False
    assert s.can_change_plan is False


@pytest.mark.parametrize(
    "role,can_change_plan_raw,is_admin,can_change_plan",
    [
        ("OWNER", None, True, True),
        ("ADMIN", None, True, True),
        ("FINANCE_ADMIN", True, False, True),
        ("SECURITY_ADMIN", None, False, False),
        ("MEMBER", None, False, False),
    ],
)
def test_parser_five_roles(
    role, can_change_plan_raw, is_admin, can_change_plan
):
    payload = {"org": {"role": role}}
    if can_change_plan_raw is not None:
        payload["canChangePlan"] = can_change_plan_raw

    state = subscription_state_from_payload(payload, portal_url=None)

    assert state.is_admin is is_admin
    assert state.can_change_plan_raw is can_change_plan_raw
    assert state.can_change_plan is can_change_plan


@pytest.mark.parametrize(
    "role,server_capability",
    [("MEMBER", True), ("OWNER", False)],
)
def test_parser_can_change_plan_prefers_server_capability(role, server_capability):
    state = subscription_state_from_payload(
        {"org": {"role": role}, "canChangePlan": server_capability},
        portal_url=None,
    )

    assert state.can_change_plan is server_capability


def test_parser_can_change_plan_falls_back_to_legacy_role_check():
    owner = subscription_state_from_payload(
        {"org": {"role": "OWNER"}}, portal_url=None
    )
    member = subscription_state_from_payload(
        {"org": {"role": "MEMBER"}}, portal_url=None
    )

    assert owner.can_change_plan is True
    assert member.can_change_plan is False


def test_parser_defaults_unknown_context_to_personal():
    s = subscription_state_from_payload({"context": "wat"}, portal_url=None)
    assert s.context == "personal"


# ── tier catalog parsing (the picker) ────────────────────────────────


def test_parser_maps_tiers_catalog():
    payload = {
        "tiers": [
            {
                "tierId": "free",
                "name": "Free",
                "tierOrder": 0,
                "dollarsPerMonthDisplay": "0",
                "monthlyCredits": "0",
                "isCurrent": False,
                "isEnabled": True,
            },
            {
                "tierId": "plus",
                "name": "Plus",
                "tierOrder": 1,
                "dollarsPerMonthDisplay": "20",
                "monthlyCredits": "1000",
                "isCurrent": True,
                "isEnabled": True,
            },
        ],
    }
    s = subscription_state_from_payload(payload, portal_url=None)
    assert len(s.tiers) == 2
    free, plus = s.tiers
    # The free tier's 0s must survive (coalesce-on-None, not falsy `or`).
    assert free.tier_id == "free" and free.tier_order == 0
    assert free.dollars_per_month == Decimal("0")
    assert plus.is_current is True
    assert plus.dollars_per_month == Decimal("20") and plus.monthly_credits == Decimal("1000")


def test_parser_tiers_absent_is_empty_tuple():
    assert subscription_state_from_payload({}, portal_url=None).tiers == ()


# ── preview parser (POST /preview) ───────────────────────────────────


def test_preview_parser_charge_now():
    p = subscription_change_preview_from_payload(
        {
            "effect": "charge_now",
            "reason": None,
            "currentTierId": "plus",
            "currentTierName": "Plus",
            "targetTierId": "ultra",
            "targetTierName": "Ultra",
            "monthlyCreditsDelta": "6000",
            "amountDueNowCents": 1234,
            "effectiveAt": None,
        }
    )
    assert p.effect == "charge_now"
    assert p.amount_due_now_cents == 1234
    assert p.target_tier_name == "Ultra"
    assert p.monthly_credits_delta == Decimal("6000")


def test_preview_parser_scheduled_has_effective_at_and_no_charge():
    p = subscription_change_preview_from_payload(
        {"effect": "scheduled", "amountDueNowCents": None, "effectiveAt": "2026-08-01"}
    )
    assert p.effect == "scheduled"
    assert p.amount_due_now_cents is None
    assert p.effective_at == "2026-08-01"


def test_preview_parser_blocked_carries_reason():
    p = subscription_change_preview_from_payload(
        {"effect": "blocked", "reason": "Retract the cancellation before upgrading."}
    )
    assert p.effect == "blocked"
    assert p.reason and "Retract" in p.reason


def test_preview_parser_missing_effect_fails_safe_to_blocked():
    # A malformed quote must never read as a charge — default to blocked.
    p = subscription_change_preview_from_payload({})
    assert p.effect == "blocked"
    assert p.amount_due_now_cents is None


# ── dev fixtures (env-driven, no live portal) ────────────────────────


def test_no_fixture_when_env_unset(monkeypatch):
    monkeypatch.delenv("HERMES_DEV_SUBSCRIPTION_FIXTURE", raising=False)
    assert dev_fixture_subscription_state() is None


@pytest.mark.parametrize(
    "name,checker",
    [
        ("free", lambda s: s.logged_in and s.current is None),
        ("mid", lambda s: s.current and s.current.tier_id == "plus"),
        ("top", lambda s: s.current and s.current.tier_id == "ultra"),
        ("not-admin", lambda s: s.role == "MEMBER" and not s.can_change_plan),
        ("downgrade", lambda s: s.current and s.current.pending_downgrade_tier_name == "Plus"),
        ("cancel", lambda s: s.current and s.current.cancel_at_period_end),
        ("team", lambda s: s.context == "team" and s.current is None),
        ("logged-out", lambda s: not s.logged_in),
    ],
)
def test_dev_fixture_states(monkeypatch, name, checker):
    monkeypatch.setenv("HERMES_DEV_SUBSCRIPTION_FIXTURE", name)
    s = dev_fixture_subscription_state()
    assert s is not None
    assert checker(s)


def test_dev_fixture_exposes_tier_catalog(monkeypatch):
    # A picker needs a catalog: the mid fixture lists tiers with the active one flagged.
    monkeypatch.setenv("HERMES_DEV_SUBSCRIPTION_FIXTURE", "mid")
    s = dev_fixture_subscription_state()
    assert s is not None and len(s.tiers) >= 2
    current = [t for t in s.tiers if t.is_current]
    assert len(current) == 1 and current[0].tier_id == "plus"


def test_dev_fixture_unknown_name_fails_safe(monkeypatch):
    monkeypatch.setenv("HERMES_DEV_SUBSCRIPTION_FIXTURE", "bogus")
    s = dev_fixture_subscription_state()
    assert s is not None
    assert s.logged_in is False
    assert s.error and "bogus" in s.error


def test_build_subscription_state_uses_fixture(monkeypatch):
    # build_subscription_state must short-circuit to the fixture (no portal call).
    monkeypatch.setenv("HERMES_DEV_SUBSCRIPTION_FIXTURE", "mid")
    s = build_subscription_state()
    assert s.logged_in is True
    assert s.current is not None and s.current.tier_id == "plus"
    # The manage URL is buildable from the fixture's portal_url + org_id.
    url = subscription_manage_url(s)
    assert url is not None
    assert url.endswith("/manage-subscription?org_id=org_acme")
