"""Regression tests for the max_trial plan profile.

The 30-day free-signup trial used to be an *uplift* (get_effective_plan returned
"max") plus a TRIAL_EXCLUDED_FEATURES set plus a special case in
get_ai_credit_limit plus an inline copy of the same logic in ai_service. Those
had already drifted: the gate capped a trial at 60 credits while ai_service read
the MAX pool of 1000.

Worse, the uplift granted BYOK, and _track_ai_usage skips credit billing
entirely when the org's own key is used (`if org and not used_own_key`) — so a
trial user could paste an OpenAI key and get 30 days of completely unmetered MAX
access, defeating the credit wall the trial converts on.

These tests pin the behaviour that closes both holes.
"""
import datetime as dt

import pytest

from supporthub.app.services import plan_service as P


class FakeOrg:
    """Duck-typed stand-in — plan_service only reads attributes."""

    def __init__(self, plan, trial_days=None, bonus_days=None, byok=False,
                 created_days_ago=90):
        self.plan = plan
        # The window is anchored to account creation, so every fixture needs one.
        self.created_at = dt.datetime.utcnow() - dt.timedelta(days=created_days_ago)
        self.trial_ends_at = (
            dt.datetime.utcnow() + dt.timedelta(days=trial_days) if trial_days else None
        )
        self.max_bonus_ends_at = (
            dt.datetime.utcnow() + dt.timedelta(days=bonus_days) if bonus_days else None
        )
        self.openai_api_key_encrypted = "fernet-token" if byok else None
        self.ai_credits_used = 0
        self.ai_credits_extra = 0
        self.extra_module_slots = 0


def test_trial_resolves_to_max_trial_not_max():
    org = FakeOrg("free", trial_days=10)
    assert P.get_effective_plan(org) == "max_trial"


def test_expired_trial_falls_back_to_stored_plan():
    org = FakeOrg("free")
    org.trial_ends_at = dt.datetime.utcnow() - dt.timedelta(days=1)
    assert P.get_effective_plan(org) == "free"


def test_trial_credit_pool_is_60_from_plan_limits():
    """The gate and PLAN_LIMITS must agree — this is the pair that drifted."""
    org = FakeOrg("free", trial_days=10)
    assert P.get_ai_credit_limit(org) == 60
    assert P.PLAN_LIMITS[P.get_effective_plan(org)]["ai_credits"] == 60


def test_byok_is_blocked_during_trial_even_with_a_key_stored():
    """The hole: BYOK bypasses credit billing, so a trial must not have it."""
    org = FakeOrg("free", trial_days=10, byok=True)
    assert P.check_feature(org, "byok") is False
    assert P.is_byok(org) is False


def test_byok_works_for_real_max_with_a_key():
    assert P.is_byok(FakeOrg("max", byok=True)) is True


def test_byok_needs_an_actual_key():
    assert P.is_byok(FakeOrg("max")) is False


def test_historical_sync_withheld_from_trial_but_kept_for_paid():
    assert P.check_feature(FakeOrg("free", trial_days=10), "historical_sync") is False
    assert P.check_feature(FakeOrg("max"), "historical_sync") is True
    assert P.check_feature(FakeOrg("premium"), "historical_sync") is True


def test_ratings_still_withheld_from_trial():
    """Previously enforced via TRIAL_EXCLUDED_FEATURES; now a max_trial flag."""
    org = FakeOrg("free", trial_days=10)
    assert P.check_feature(org, "ratings") is False
    assert P.check_feature(org, "ratings_export") is False
    assert P.check_feature(FakeOrg("max"), "ratings") is True


def test_trial_still_gets_the_features_that_sell_the_product():
    org = FakeOrg("free", trial_days=10)
    for feature in ("rag", "semantic_search", "doc_scraping", "analytics",
                    "order_sync", "generated_guides", "export_conversation"):
        assert P.check_feature(org, feature) is True, feature


def test_paying_premium_max_bonus_gets_real_max_not_trial():
    """The MAX bonus is for people who paid — it must not be downgraded to trial."""
    org = FakeOrg("premium", bonus_days=10)
    assert P.get_effective_plan(org) == "max"
    assert P.check_feature(org, "byok") is True


@pytest.mark.parametrize("plan", sorted(P.PLAN_FEATURES))
def test_every_plan_declares_every_feature(plan):
    """A new MAX feature must not silently leak to trials by defaulting to absent.

    check_feature() returns False for a missing key, so an omission fails closed
    rather than open — but it also means a plan could silently lose a feature.
    Keeping the key sets identical forces an explicit decision per plan.
    """
    assert set(P.PLAN_FEATURES[plan]) == set(P.PLAN_FEATURES["max"])


@pytest.mark.parametrize("plan", sorted(P.PLAN_LIMITS))
def test_every_plan_declares_every_limit(plan):
    assert set(P.PLAN_LIMITS[plan]) == set(P.PLAN_LIMITS["max"])


# ── History window (trial + downgrade) ───────────────────────────────────────

def test_trial_and_free_get_a_30_day_window_paid_plans_unlimited():
    assert P.get_history_days(FakeOrg("free", trial_days=10)) == 30
    assert P.get_history_days(FakeOrg("free")) == 30
    assert P.get_history_days(FakeOrg("premium")) is None
    assert P.get_history_days(FakeOrg("max")) is None
    assert P.get_history_days(FakeOrg("internal")) is None


def test_history_cutoff_is_anchored_to_account_creation_not_today():
    """A rolling cutoff would make imported history evaporate over time."""
    org = FakeOrg("free", created_days_ago=90)
    cutoff = P.get_history_cutoff(org)
    assert cutoff is not None
    assert cutoff == org.created_at - dt.timedelta(days=30)
    # i.e. 120 days before now, NOT 30
    assert (dt.datetime.utcnow() - cutoff).days == 120


def test_history_cutoff_does_not_move_as_time_passes():
    """The anchor is fixed: the same org must yield the same cutoff regardless
    of how long ago it signed up, so nothing ever falls OUT of the window."""
    young = FakeOrg("free", created_days_ago=1)
    old = FakeOrg("free", created_days_ago=365)
    assert P.get_history_cutoff(young) == young.created_at - dt.timedelta(days=30)
    assert P.get_history_cutoff(old) == old.created_at - dt.timedelta(days=30)


def test_data_imported_at_signup_stays_visible_much_later():
    """The regression that motivated the anchor: a thread 20 days old at signup
    must still be in-window 200 days later."""
    org = FakeOrg("free", created_days_ago=200)
    thread_activity = org.created_at - dt.timedelta(days=20)
    assert thread_activity >= P.get_history_cutoff(org)


def test_history_cutoff_is_none_for_paid_plans():
    """None must mean 'no restriction' — every read path branches on this."""
    assert P.get_history_cutoff(FakeOrg("max")) is None
    assert P.get_history_cutoff(FakeOrg("premium")) is None


def test_upgrade_restores_full_history_immediately():
    """Downgrade hides, it never deletes: flipping the plan back removes the cutoff."""
    org = FakeOrg("free")
    assert P.get_history_cutoff(org) is not None
    org.plan = "max"
    assert P.get_history_cutoff(org) is None


def test_order_import_start_date_is_clamped_into_the_window():
    org = FakeOrg("free")
    clamped = P.clamp_history_date(org, "2010-01-01")
    cutoff = P.get_history_cutoff(org)
    assert clamped == cutoff.strftime("%Y-%m-%d")


def test_order_import_start_date_untouched_for_paid_plans():
    assert P.clamp_history_date(FakeOrg("max"), "2020-01-01") == "2020-01-01"


def test_a_recent_requested_date_is_not_pushed_backwards():
    """Clamping must only move the date FORWARD, never widen the request."""
    org = FakeOrg("free", created_days_ago=90)
    recent = (dt.datetime.utcnow() - dt.timedelta(days=3)).strftime("%Y-%m-%d")
    assert P.clamp_history_date(org, recent) == recent


def test_months_back_import_is_clamped_to_one_month_on_free_and_trial():
    assert P.clamp_history_months(FakeOrg("free"), 120) == 1
    assert P.clamp_history_months(FakeOrg("free", trial_days=10), 24) == 1
    assert P.clamp_history_months(FakeOrg("max"), 120) == 120


def test_months_clamp_never_returns_zero():
    assert P.clamp_history_months(FakeOrg("free"), 1) == 1


# ── None-safety of the gate helpers ──────────────────────────────────────────
# AIService._org is None during a sync. check_feature(None, ...) used to raise
# AttributeError, which the caller swallowed with `except Exception` — so every
# AI classification during a sync failed silently and auto-priority /
# review-request suggestions never fired at all.

def test_gate_helpers_are_none_safe_and_fail_closed():
    assert P.get_plan(None) == "free"
    assert P.get_effective_plan(None) == "free"
    assert P.check_feature(None, "auto_language_detect") is False
    assert P.check_feature(None, "ratings") is False
    assert P.check_feature(None, "byok") is False


def test_none_org_limits_do_not_raise():
    assert P.get_history_days(None) == 30
    assert P.get_history_cutoff(None) is None       # no created_at to anchor to
    assert P.get_ai_credit_limit(None) == 25
    assert P.is_byok(None) is False


def test_unknown_stored_plan_falls_back_to_free():
    assert P.get_plan(FakeOrg("nonsense-plan")) == "free"
