import pytest

from health_client import AccountSnapshot, HealthStatus
from limit_warning import WINDOW_7D
from routing_resolver import NoHealthyAccountError, RoutingResolver, RoutingRules


def make_rules(**kwargs: object) -> RoutingRules:
    defaults = {
        "projects": {"zync.is": "avi", "automixer": "rafa", "multideal": "roy"},
        "default": "rafa",
        "fallback_chain": ["roy", "avi"],
        "fallback_trigger": "broken_or_quota_exhausted",
        "quota_exhausted_threshold_pct": 100,
    }
    defaults.update(kwargs)
    return RoutingRules(**defaults)


def test_project_override_rejects_unknown_registry_slug() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {"rafa": AccountSnapshot(HealthStatus.OK, 1, 2)},
        known_slugs={"rafa", "roy"},
    )

    try:
        resolver.resolve("zync.is")
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "avi" in message
    assert "unknown-account" in message
    assert "rafa" not in message


def test_project_override_returns_pinned_slug_when_healthy() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "avi": AccountSnapshot(HealthStatus.OK, 12, 34),
            "rafa": AccountSnapshot(HealthStatus.OK, 1, 2),
        },
    )

    route = resolver.resolve("zync.is")

    assert route.slug == "avi"
    assert route.fallback_used is False
    assert route.fallback_from is None


def test_known_registry_slug_without_health_still_resolves() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {"rafa": AccountSnapshot(HealthStatus.OK, 1, 2)},
        known_slugs={"avi", "rafa", "roy"},
    )

    route = resolver.resolve("unknown-project")

    assert route.slug == "rafa"
    assert route.fallback_used is False
    assert route.fallback_from is None


def test_unavailable_project_override_raises_without_global_fallback() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "avi": AccountSnapshot(HealthStatus.BROKEN, None, None),
            "rafa": AccountSnapshot(HealthStatus.OK, 1, 2),
            "roy": AccountSnapshot(HealthStatus.OK, 3, 4),
        },
    )

    try:
        resolver.resolve("zync.is")
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "avi" in message
    assert "broken" in message
    assert "rafa" not in message
    assert "roy" not in message


def test_healthy_default_returns_default_without_fallback() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {"rafa": AccountSnapshot(HealthStatus.OK, 10, 20)},
    )

    route = resolver.resolve("unknown-project")

    assert route.slug == "rafa"
    assert route.fallback_used is False
    assert route.fallback_from is None


def test_broken_default_uses_first_available_fallback() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.BROKEN, None, None),
            "roy": AccountSnapshot(HealthStatus.OK, 33, 44),
            "avi": AccountSnapshot(HealthStatus.OK, 55, 66),
        },
    )

    route = resolver.resolve(None)

    assert route.slug == "roy"
    assert route.fallback_used is True
    assert route.fallback_from == "rafa"


def test_quota_exhausted_default_uses_fallback_at_threshold() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 100, 20),
            "roy": AccountSnapshot(HealthStatus.OK, 99, 99),
        },
    )

    route = resolver.resolve(None)

    assert route.slug == "roy"
    assert route.fallback_used is True
    assert route.fallback_from == "rafa"


def test_missing_health_entry_is_treated_as_unavailable() -> None:
    resolver = RoutingResolver(make_rules(), {})

    try:
        resolver.resolve("unknown-project")
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "rafa=missing-health" in message
    assert "roy=missing-health" in message
    assert "avi=missing-health" in message


def test_all_unavailable_raises_with_every_candidate_status() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.BROKEN, None, None),
            "roy": AccountSnapshot(HealthStatus.OK, 100, 10),
            "avi": AccountSnapshot(HealthStatus.OK, 10, 100),
        },
    )

    try:
        resolver.resolve("unknown-project")
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "rafa" in message
    assert "roy" in message
    assert "avi" in message
    assert "broken" in message
    assert "quota-exhausted" in message


def test_capped_account_is_skipped_and_chain_falls_through() -> None:
    resolver = RoutingResolver(
        make_rules(account_caps={"rafa": {WINDOW_7D: 10}}),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 10, 90, secondary_reset_at=1786365653.0),
            "roy": AccountSnapshot(HealthStatus.OK, 10, 20),
        },
        known_slugs={"rafa", "roy"},
    )

    route = resolver.resolve(None)

    assert route.slug == "roy"
    assert route.fallback_used is True
    assert route.fallback_from == "rafa"


def test_capped_status_uses_describe_caps_not_quota_exhausted() -> None:
    resolver = RoutingResolver(
        make_rules(
            account_caps={"rafa": {WINDOW_7D: 10}},
            quota_exhausted_threshold_pct=90,
        ),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 10, 90),
        },
        known_slugs={"rafa"},
    )

    try:
        resolver.resolve(None)
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "capped(7d=10% left <= 10%)" in message
    assert "quota-exhausted" not in message


def test_missing_health_does_not_admit_capped_account() -> None:
    resolver = RoutingResolver(
        make_rules(
            projects={},
            default="rafa",
            fallback_chain=[],
            account_caps={"rafa": {WINDOW_7D: 10}},
            missing_health_is_available=True,
        ),
        {},
        known_slugs={"rafa"},
    )

    assert resolver._is_available("rafa") is False

    try:
        resolver.resolve(None)
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "rafa=missing-health" in message


def test_uncapped_quota_exhausted_regression_unchanged() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 100, 20),
            "roy": AccountSnapshot(HealthStatus.OK, 99, 99),
        },
    )

    route = resolver.resolve(None)

    assert route.slug == "roy"
    assert route.fallback_used is True
    assert route.fallback_from == "rafa"


def test_zync2_remaining_cap_inversion_regression() -> None:
    caps = {WINDOW_7D: 10}
    rules = make_rules(
        projects={},
        default="zync2",
        fallback_chain=[],
        account_caps={"zync2": caps},
    )
    available = RoutingResolver(
        rules,
        {
            "zync2": AccountSnapshot(
                HealthStatus.OK,
                None,
                88,
                secondary_reset_at=1786365653.0,
            ),
        },
        known_slugs={"zync2"},
    )
    refused = RoutingResolver(
        rules,
        {
            "zync2": AccountSnapshot(
                HealthStatus.OK,
                None,
                90,
                secondary_reset_at=1786365653.0,
            ),
        },
        known_slugs={"zync2"},
    )

    assert available.resolve(None).slug == "zync2"

    try:
        refused.resolve(None)
    except NoHealthyAccountError as exc:
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert "capped(7d=10% left <= 10%)" in message


def test_all_cap_excluded_detects_every_candidate_capped() -> None:
    resolver = RoutingResolver(
        make_rules(
            projects={},
            default="rafa",
            fallback_chain=["roy"],
            account_caps={"rafa": {WINDOW_7D: 10}, "roy": {WINDOW_7D: 10}},
        ),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 10, 90),
            "roy": AccountSnapshot(HealthStatus.OK, 10, 95),
        },
        known_slugs={"rafa", "roy"},
    )

    try:
        resolver.resolve(None)
    except NoHealthyAccountError as exc:
        chain = exc.chain
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert resolver.all_cap_excluded(chain) is True


def test_all_cap_excluded_false_when_quota_exhausted_without_caps() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 100, 100),
            "roy": AccountSnapshot(HealthStatus.OK, 100, 100),
        },
    )

    try:
        resolver.resolve(None)
    except NoHealthyAccountError as exc:
        chain = exc.chain
        message = str(exc)
    else:
        raise AssertionError("expected NoHealthyAccountError")

    assert resolver.all_cap_excluded(chain) is False
    assert "quota-exhausted" in message
    assert "capped(" not in message


def test_earliest_cap_resume_at_uses_earliest_reset() -> None:
    resolver = RoutingResolver(
        make_rules(
            account_caps={
                "rafa": {WINDOW_7D: 10},
                "roy": {WINDOW_7D: 10},
            },
        ),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 10, 90, secondary_reset_at=2000.0),
            "roy": AccountSnapshot(HealthStatus.OK, 10, 95, secondary_reset_at=1000.0),
        },
    )

    assert resolver.earliest_cap_resume_at(("rafa", "roy")) == "1970-01-01T00:16:40Z"


def test_locked_default_is_skipped_before_health_and_falls_through() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "rafa": AccountSnapshot(HealthStatus.OK, 1, 2),
            "roy": AccountSnapshot(HealthStatus.OK, 3, 4),
        },
        known_slugs={"rafa", "roy"},
        locked_slugs={"rafa"},
    )

    route = resolver.resolve(None)

    assert route.slug == "roy"
    assert route.fallback_used is True
    assert route.fallback_from == "rafa"


def test_locked_project_pin_is_not_replaced_by_global_fallback() -> None:
    resolver = RoutingResolver(
        make_rules(),
        {
            "avi": AccountSnapshot(HealthStatus.OK, 1, 2),
            "rafa": AccountSnapshot(HealthStatus.OK, 3, 4),
        },
        known_slugs={"avi", "rafa"},
        locked_slugs={"avi"},
    )

    with pytest.raises(NoHealthyAccountError) as denied:
        resolver.resolve("zync.is")

    assert "avi=locked" in str(denied.value)
    assert resolver.all_locked(denied.value.chain) is True
    assert resolver.unlocked(denied.value.chain) == ()
