from __future__ import annotations

import json
import stat
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import authority_client
import provider_services
from account_registry import (
    Account,
    AccountRef,
    AccountRegistryKind,
    AuthorityBinding,
    AuthorityMode,
)
from authority_client import (
    AuthorityConfigurationError,
    GatewayStatus,
    build_authority_launch,
    fetch_gateway_status,
    gateway_health_snapshot,
    materialize_gateway_home,
    resolve_authority_endpoint,
)
from health_client import AccountSnapshot, HealthStatus
from provider_services import (
    AuthorityHealthAdapter,
    ProviderServiceMap,
    ProviderServices,
)


_READY_STATUS = {
    "state": "ready",
    "provider": "codex",
    "route_fingerprint": authority_client._fingerprint("route-fixture"),
    "grant_fingerprint": "0123456789ab",
    "grant_expires_at": "2099-01-01T00:00:00Z",
    "grant_revoked": False,
    "account_availability": "available",
}


class _StatusHandler(BaseHTTPRequestHandler):
    body = dict(_READY_STATUS)
    requests: list[tuple[str, str | None]] = []

    def do_GET(self) -> None:
        self.__class__.requests.append((self.path, self.headers.get("Authorization")))
        payload = json.dumps(self.__class__.body).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def log_message(self, _format: str, *_args: object) -> None:
        return


@pytest.fixture
def authority(tmp_path: Path):
    _StatusHandler.requests = []
    _StatusHandler.body = dict(_READY_STATUS)
    server = ThreadingHTTPServer(("127.0.0.1", 0), _StatusHandler)
    thread = threading.Thread(target=server.serve_forever)
    thread.start()
    base_dir = tmp_path / "runtime"
    base_dir.mkdir(mode=0o700)
    base_dir.chmod(0o700)
    origin = f"http://127.0.0.1:{server.server_port}"
    endpoints = base_dir / "authority_endpoints.json"
    endpoints.write_text(
        json.dumps({"authorities": {"workstation": {"origin": origin}}}),
        encoding="utf-8",
    )
    endpoints.chmod(0o600)
    grant = tmp_path / "fixture.grant"
    grant.write_text("synthetic-route-key", encoding="utf-8")
    grant.chmod(0o600)
    binding = AuthorityBinding(
        mode=AuthorityMode.SUBROUTER_DARK,
        authority_name="workstation",
        route_id="route-fixture",
        provider="codex",
        proxy_grant_ref=grant.resolve(),
    )
    account = Account(
        ref=AccountRef("codex", "fixture"),
        alias="Fixture",
        account_home=tmp_path / "native" / "CODEX_HOME",
        email=None,
        plan=None,
        account_id=None,
        authority_binding=binding,
    )
    try:
        yield base_dir, account, binding, origin
    finally:
        server.shutdown()
        thread.join()
        server.server_close()


def test_build_authority_launch_uses_gateway_only_home_and_strips_native_auth(
    authority,
) -> None:
    base_dir, account, _binding, origin = authority

    launch = build_authority_launch(
        base_dir,
        account,
        "codex",
        ["exec", "task"],
        inherited_environment={
            "OPENAI_API_KEY": "must-not-survive",
            "OPENAI_BASE_URL": "https://wrong.invalid",
            "ANTHROPIC_AUTH_TOKEN": "must-not-survive",
            "ANTHROPIC_BASE_URL": "https://wrong.invalid",
            "CLAUDE_CONFIG_DIR": "/native",
            "CODEX_HOME": "/native",
            "SUBROUTER_PROXY_KEY": "wrong-grant",
            "HTTPS_PROXY": "http://wrong.invalid",
            "SAFE": "preserved",
        },
    )

    assert launch.environment["SAFE"] == "preserved"
    assert "OPENAI_API_KEY" not in launch.environment
    assert "OPENAI_BASE_URL" not in launch.environment
    assert "ANTHROPIC_AUTH_TOKEN" not in launch.environment
    assert "ANTHROPIC_BASE_URL" not in launch.environment
    assert "CLAUDE_CONFIG_DIR" not in launch.environment
    assert "HTTPS_PROXY" not in launch.environment
    assert launch.environment["SUBROUTER_PROXY_KEY"] == "synthetic-route-key"
    assert launch.environment["HOME"] == str(launch.gateway_home)
    assert launch.environment["CODEX_HOME"] == str(launch.gateway_home)
    assert launch.gateway_home != account.account_home
    assert stat.S_IMODE(launch.gateway_home.stat().st_mode) == 0o700
    assert sorted(path.name for path in launch.gateway_home.iterdir()) == [
        "config.toml",
        "gateway-manifest.json",
    ]
    assert not any((launch.gateway_home / name).exists() for name in ("auth.json", ".credentials.json", "claude.json"))
    config = (launch.gateway_home / "config.toml").read_text(encoding="utf-8")
    assert f'{origin}/r/route-fixture/v1' in config
    assert "synthetic-route-key" not in config
    manifest = json.loads(
        (launch.gateway_home / "gateway-manifest.json").read_text(encoding="utf-8")
    )
    assert manifest["provider_auth_files_present"] is False
    assert "route-fixture" not in json.dumps(manifest)
    assert _StatusHandler.requests == [
        ("/r/route-fixture/_subrouter/status", "Bearer synthetic-route-key")
    ]


def test_launch_uses_one_grant_snapshot_for_status_and_execution(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, account, _binding, _origin = authority
    reads: list[str] = []
    status_keys: list[str] = []

    def read_once(_path: Path) -> str:
        value = "first-key" if not reads else "second-key"
        reads.append(value)
        return value

    def status_with_key(
        _endpoint: object,
        _binding: object,
        key: str,
        *,
        timeout_secs: float,
    ) -> GatewayStatus:
        del timeout_secs
        status_keys.append(key)
        return GatewayStatus("ready", "Gateway: ready", 1.0)

    monkeypatch.setattr(authority_client, "_read_proxy_key", read_once)
    monkeypatch.setattr(authority_client, "_fetch_gateway_status", status_with_key)

    launch = build_authority_launch(base_dir, account, "codex", ["exec", "task"])

    assert reads == ["first-key"]
    assert status_keys == ["first-key"]
    assert launch.environment["SUBROUTER_PROXY_KEY"] == "first-key"


def test_claude_launch_uses_gateway_only_home_and_exact_route(authority) -> None:
    base_dir, _account, binding, origin = authority
    claude_binding = AuthorityBinding(
        mode=binding.mode,
        authority_name=binding.authority_name,
        route_id=binding.route_id,
        provider="claude",
        proxy_grant_ref=binding.proxy_grant_ref,
    )
    account = Account(
        ref=AccountRef("claude", "fixture"),
        alias="Fixture",
        account_home=base_dir / "native" / "CLAUDE_CONFIG_DIR",
        email=None,
        plan=None,
        account_id=None,
        authority_binding=claude_binding,
    )

    _StatusHandler.body = {**_READY_STATUS, "provider": "claude"}
    launch = build_authority_launch(
        base_dir,
        account,
        "claude",
        ["--print", "task"],
        inherited_environment={
            "CLAUDE_CONFIG_DIR": "/native",
            "CLAUDE_CODE_OAUTH_TOKEN": "remove",
            "SUBROUTER_PROXY_KEY": "wrong-grant",
            "OPENAI_BASE_URL": "https://wrong.invalid",
        },
    )

    assert launch.environment["CLAUDE_CONFIG_DIR"] == str(launch.gateway_home)
    assert launch.environment["ANTHROPIC_BASE_URL"] == f"{origin}/r/route-fixture"
    assert launch.environment["ANTHROPIC_AUTH_TOKEN"] == "synthetic-route-key"
    assert "CLAUDE_CODE_OAUTH_TOKEN" not in launch.environment
    assert "SUBROUTER_PROXY_KEY" not in launch.environment
    assert "OPENAI_BASE_URL" not in launch.environment
    assert launch.environment["HOME"] == str(launch.gateway_home)
    assert sorted(path.name for path in launch.gateway_home.iterdir()) == [
        "gateway-manifest.json"
    ]
    assert not any((launch.gateway_home / name).exists() for name in ("auth.json", ".credentials.json", "claude.json"))


def test_gateway_home_refuses_provider_auth_files(authority) -> None:
    base_dir, account, binding, _origin = authority
    endpoint = resolve_authority_endpoint(base_dir, binding)
    root = base_dir / "gateway-homes" / "codex"
    first = materialize_gateway_home(base_dir, account, endpoint, binding)
    (first / "auth.json").write_text("{}", encoding="utf-8")

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        materialize_gateway_home(base_dir, account, endpoint, binding)

    assert root.exists()


def test_gateway_home_interrupted_materialization_publishes_nothing(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, account, binding, _origin = authority
    rebound = Account(
        ref=account.ref,
        alias=account.alias,
        account_home=account.account_home,
        email=account.email,
        plan=account.plan,
        account_id=account.account_id,
        authority_binding=AuthorityBinding(
            mode=binding.mode,
            authority_name=binding.authority_name,
            route_id="route-interrupted",
            provider=binding.provider,
            proxy_grant_ref=binding.proxy_grant_ref,
        ),
    )
    endpoint = resolve_authority_endpoint(base_dir, rebound.authority_binding)
    original_write = authority_client._atomic_write
    writes = 0

    def interrupted_write(path: Path, body: str, mode: int) -> None:
        nonlocal writes
        writes += 1
        if writes == 2:
            raise OSError("interrupted")
        original_write(path, body, mode)

    monkeypatch.setattr(authority_client, "_atomic_write", interrupted_write)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        materialize_gateway_home(base_dir, rebound, endpoint, rebound.authority_binding)

    root = authority_client._gateway_root(base_dir, rebound, rebound.authority_binding)
    assert not root.exists()
    assert not list(root.parent.glob(f".{root.name}.*"))


def test_gateway_status_is_sanitized_and_missing_grant_fails_closed(authority) -> None:
    base_dir, account, binding, _origin = authority

    snapshot = gateway_health_snapshot(base_dir, account)

    assert snapshot.status == HealthStatus.OK
    assert snapshot.detail == "Gateway: ready"
    binding.proxy_grant_ref.unlink()

    unavailable = gateway_health_snapshot(base_dir, account)

    assert unavailable.status == HealthStatus.UNKNOWN
    assert unavailable.detail == "Gateway: migration required"
    assert "fixture" not in unavailable.detail


def test_gateway_status_maps_exact_exhausted_state(authority) -> None:
    base_dir, _account, binding, _origin = authority
    _StatusHandler.body = {
        **_READY_STATUS,
        "state": "exhausted",
        "account_availability": "exhausted",
    }

    status = fetch_gateway_status(base_dir, binding)

    assert status.state == "exhausted"
    assert status.detail == "Gateway: quota exhausted"


@pytest.mark.parametrize(
    ("field", "value"),
    [
        ("provider", "claude"),
        ("route_fingerprint", "ffffffffffff"),
        ("grant_fingerprint", "not-bounded"),
        ("grant_expires_at", "2000-01-01T00:00:00Z"),
        ("grant_revoked", True),
        ("account_availability", "unavailable"),
        ("credential", "must-be-rejected"),
    ],
)
def test_gateway_status_rejects_wrong_or_unbounded_schema(
    authority,
    field: str,
    value: object,
) -> None:
    base_dir, _account, binding, _origin = authority
    _StatusHandler.body = {**_READY_STATUS, field: value}

    with pytest.raises(AuthorityConfigurationError, match="unavailable"):
        fetch_gateway_status(base_dir, binding)


def test_gateway_status_refuses_redirect_without_forwarding_grant(authority) -> None:
    base_dir, _account, binding, _origin = authority
    captured: list[str | None] = []

    class _Sink(BaseHTTPRequestHandler):
        def do_GET(self) -> None:
            captured.append(self.headers.get("Authorization"))
            self.send_response(200)
            self.end_headers()

        def log_message(self, _format: str, *_args: object) -> None:
            return

    sink = ThreadingHTTPServer(("127.0.0.1", 0), _Sink)

    class _Redirect(BaseHTTPRequestHandler):
        def do_GET(self) -> None:
            self.send_response(302)
            self.send_header(
                "Location", f"http://127.0.0.1:{sink.server_port}/capture"
            )
            self.end_headers()

        def log_message(self, _format: str, *_args: object) -> None:
            return

    redirect = ThreadingHTTPServer(("127.0.0.1", 0), _Redirect)
    threads = [
        threading.Thread(target=sink.serve_forever),
        threading.Thread(target=redirect.serve_forever),
    ]
    for thread in threads:
        thread.start()
    try:
        endpoints = base_dir / "authority_endpoints.json"
        endpoints.write_text(
            json.dumps(
                {
                    "authorities": {
                        "workstation": {
                            "origin": f"http://127.0.0.1:{redirect.server_port}"
                        }
                    }
                }
            ),
            encoding="utf-8",
        )

        with pytest.raises(AuthorityConfigurationError, match="unavailable"):
            fetch_gateway_status(base_dir, binding)

        assert captured == []
    finally:
        redirect.shutdown()
        sink.shutdown()
        for thread in threads:
            thread.join()
        redirect.server_close()
        sink.server_close()


def test_binding_rejects_permissive_or_symlinked_grant(authority, tmp_path: Path) -> None:
    base_dir, account, binding, _origin = authority
    binding.proxy_grant_ref.chmod(0o644)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        build_authority_launch(base_dir, account, "codex", [])

    target = tmp_path / "target"
    target.write_text("synthetic", encoding="utf-8")
    target.chmod(0o600)
    binding.proxy_grant_ref.unlink()
    binding.proxy_grant_ref.symlink_to(target)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        build_authority_launch(base_dir, account, "codex", [])


def test_binding_rejects_hard_linked_grant(authority, tmp_path: Path) -> None:
    base_dir, account, binding, _origin = authority
    original = tmp_path / "original.grant"
    binding.proxy_grant_ref.unlink()
    original.write_text("synthetic", encoding="utf-8")
    original.chmod(0o600)
    binding.proxy_grant_ref.hardlink_to(original)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        build_authority_launch(base_dir, account, "codex", [])


def test_binding_rejects_intermediate_symlink_in_grant_path(
    authority, tmp_path: Path
) -> None:
    base_dir, account, binding, _origin = authority
    safe = tmp_path / "safe"
    safe.mkdir(mode=0o700)
    outside = tmp_path / "outside"
    outside.mkdir(mode=0o700)
    grant = outside / "grant"
    grant.write_text("synthetic", encoding="utf-8")
    grant.chmod(0o600)
    (safe / "link").symlink_to(outside, target_is_directory=True)
    rebound = Account(
        ref=account.ref,
        alias=account.alias,
        account_home=account.account_home,
        email=account.email,
        plan=account.plan,
        account_id=account.account_id,
        authority_binding=AuthorityBinding(
            mode=binding.mode,
            authority_name=binding.authority_name,
            route_id=binding.route_id,
            provider=binding.provider,
            proxy_grant_ref=(safe / "link" / "grant").absolute(),
        ),
    )

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        build_authority_launch(base_dir, rebound, "codex", [])


def test_endpoint_configuration_refuses_writable_or_symlinked_file(
    authority, tmp_path: Path
) -> None:
    base_dir, _account, binding, _origin = authority
    endpoints = base_dir / "authority_endpoints.json"
    endpoints.chmod(0o622)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        resolve_authority_endpoint(base_dir, binding)

    target = tmp_path / "endpoints.json"
    target.write_text(endpoints.read_text(encoding="utf-8"), encoding="utf-8")
    target.chmod(0o600)
    endpoints.unlink()
    endpoints.symlink_to(target)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        resolve_authority_endpoint(base_dir, binding)


def test_endpoint_configuration_refuses_cleartext_non_loopback(
    authority,
) -> None:
    base_dir, _account, binding, _origin = authority
    endpoints = base_dir / "authority_endpoints.json"
    endpoints.write_text(
        json.dumps(
            {"authorities": {"workstation": {"origin": "http://192.0.2.1:31415"}}}
        ),
        encoding="utf-8",
    )
    endpoints.chmod(0o600)

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        resolve_authority_endpoint(base_dir, binding)


def test_gateway_health_retains_last_verified_snapshot_as_stale(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, account, _binding, _origin = authority

    class _Registry:
        def __init__(self, path: Path) -> None:
            self.base_dir = path

    snapshots = iter(
        (
            AccountSnapshot(
                status=HealthStatus.OK,
                primary_used_pct=23,
                secondary_used_pct=7,
                checked_at=10.0,
                detail="Gateway: ready",
            ),
            AccountSnapshot(
                status=HealthStatus.UNKNOWN,
                primary_used_pct=None,
                secondary_used_pct=None,
                checked_at=20.0,
                detail="Gateway: unavailable",
            ),
        )
    )
    monkeypatch.setattr(
        provider_services,
        "gateway_health_snapshot",
        lambda *_args, **_kwargs: next(snapshots),
    )
    adapter = AuthorityHealthAdapter(_Registry(base_dir))  # type: ignore[arg-type]

    assert adapter.fetch(account).status == HealthStatus.OK
    stale = adapter.fetch(account)

    assert stale.status == HealthStatus.UNKNOWN
    assert stale.primary_used_pct == 23
    assert stale.secondary_used_pct == 7
    assert stale.checked_at == 10.0
    assert stale.detail == "Gateway: unavailable; last verified status is stale"


def test_gateway_health_cache_does_not_cross_route_rebinding(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, account, binding, _origin = authority

    class _Registry:
        def __init__(self, path: Path) -> None:
            self.base_dir = path

    snapshots = iter(
        (
            AccountSnapshot(HealthStatus.OK, 23, 7, checked_at=10.0, detail="Gateway: ready"),
            AccountSnapshot(
                HealthStatus.UNKNOWN,
                None,
                None,
                checked_at=20.0,
                detail="Gateway: unavailable",
            ),
        )
    )
    monkeypatch.setattr(
        provider_services,
        "gateway_health_snapshot",
        lambda *_args, **_kwargs: next(snapshots),
    )
    adapter = AuthorityHealthAdapter(_Registry(base_dir))  # type: ignore[arg-type]
    assert adapter.fetch(account).status == HealthStatus.OK
    rebound = Account(
        ref=account.ref,
        alias=account.alias,
        account_home=account.account_home,
        email=account.email,
        plan=account.plan,
        account_id=account.account_id,
        authority_binding=AuthorityBinding(
            mode=binding.mode,
            authority_name=binding.authority_name,
            route_id="route-other",
            provider=binding.provider,
            proxy_grant_ref=binding.proxy_grant_ref,
        ),
    )

    unavailable = adapter.fetch(rebound)

    assert unavailable.status == HealthStatus.UNKNOWN
    assert unavailable.primary_used_pct is None
    assert unavailable.checked_at == 20.0
    assert unavailable.detail == "Gateway: unavailable"


def test_dark_binding_preserves_native_health_and_adds_gateway_status(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, account, _binding, _origin = authority

    class _Registry:
        def __init__(self, path: Path) -> None:
            self.base_dir = path

    class _NativeHealth:
        def fetch(self, _account: Account, timeout_secs: float = 10.0) -> AccountSnapshot:
            del timeout_secs
            return AccountSnapshot(
                HealthStatus.OK,
                12,
                34,
                checked_at=5.0,
                detail="Native: ready",
            )

    monkeypatch.setattr(
        provider_services,
        "gateway_health_snapshot",
        lambda *_args, **_kwargs: AccountSnapshot(
            HealthStatus.UNKNOWN,
            None,
            None,
            checked_at=20.0,
            detail="Gateway: unavailable",
        ),
    )
    native_health = _NativeHealth()
    services = ProviderServices(
        tool=AccountRegistryKind.CODEX,
        registry=_Registry(base_dir),  # type: ignore[arg-type]
        lifecycle=object(),  # type: ignore[arg-type]
        health=native_health,
    )
    selected = ProviderServiceMap((services,)).for_account(account)

    snapshot = selected.health.fetch(account)

    assert snapshot.status == HealthStatus.OK
    assert snapshot.primary_used_pct == 12
    assert snapshot.secondary_used_pct == 34
    assert snapshot.checked_at == 5.0
    assert snapshot.detail == "Native: ready; Gateway: unavailable"


def test_resolve_authority_data_plane_returns_exact_responses_route_and_verified_expiry(
    authority,
) -> None:
    base_dir, _account, binding, origin = authority
    active = AuthorityBinding(
        mode=AuthorityMode.SUBROUTER,
        authority_name=binding.authority_name,
        route_id=binding.route_id,
        provider=binding.provider,
        proxy_grant_ref=binding.proxy_grant_ref,
    )

    data_plane = authority_client.resolve_authority_data_plane(base_dir, active)

    assert data_plane.responses_url == f"{origin}/r/route-fixture/v1/responses"
    assert data_plane.proxy_key == "synthetic-route-key"
    assert data_plane.grant_expires_at_ms > 4_000_000_000_000
    assert data_plane.sanitized_status.state == "ready"
    assert data_plane.sanitized_status.grant_expires_at_ms == data_plane.grant_expires_at_ms
    assert _StatusHandler.requests == [
        ("/r/route-fixture/_subrouter/status", "Bearer synthetic-route-key")
    ]


def test_resolve_authority_data_plane_refuses_dark_or_quiesced_before_reading_grant(
    authority, monkeypatch: pytest.MonkeyPatch
) -> None:
    base_dir, _account, binding, _origin = authority
    monkeypatch.setattr(
        authority_client,
        "_read_proxy_key",
        lambda *_args, **_kwargs: pytest.fail("invalid binding read grant material"),
    )

    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        authority_client.resolve_authority_data_plane(base_dir, binding)

    quiesced = AuthorityBinding(
        mode=AuthorityMode.SUBROUTER,
        authority_name=binding.authority_name,
        route_id=binding.route_id,
        provider=binding.provider,
        proxy_grant_ref=binding.proxy_grant_ref,
        quiesced=True,
    )
    with pytest.raises(AuthorityConfigurationError, match="migration required"):
        authority_client.resolve_authority_data_plane(base_dir, quiesced)
