"""Tests for viewer auth state and the relay client mapping."""

from __future__ import annotations

import stat
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

import pytest

from strix.interface.viewer import auth


def _iso(delta: timedelta) -> str:
    return (datetime.now(UTC) + delta).isoformat()


if TYPE_CHECKING:
    from pathlib import Path


@pytest.fixture(autouse=True)
def _tmp_auth(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    home = tmp_path / "home"
    monkeypatch.setattr(auth, "AUTH_PATH", home / ".strix" / "viewer-auth.json")
    return auth.AUTH_PATH


def test_write_read_forget_roundtrip() -> None:
    assert auth.read_auth() is None
    assert auth.is_verified() is False

    auth.write_auth(email="user@example.com", token="tok-123", verified_at=_iso(timedelta(days=30)))  # nosec B106

    record = auth.read_auth()
    assert record is not None
    assert record["email"] == "user@example.com"
    assert record["token"] == "tok-123"
    assert auth.is_verified() is True

    auth.forget()
    assert auth.read_auth() is None
    assert auth.is_verified() is False
    # Forget is a no-op when the file is already gone.
    auth.forget()


def test_is_verified_enforces_expiry() -> None:
    # An expired record still reads back, but no longer unlocks history.
    auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=-1)))  # nosec B106
    assert auth.read_auth() is not None
    assert auth.is_verified() is False

    # A future expiry unlocks it.
    auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=1)))  # nosec B106
    assert auth.is_verified() is True


def test_is_verified_fails_closed_when_expiry_absent_or_unparseable() -> None:
    # No/blank expiry: fail closed rather than unlocking history forever.
    auth.write_auth(email="a@b.com", token="t", verified_at="")  # nosec B106
    assert auth.read_auth() is not None
    assert auth.is_verified() is False

    # Garbage expiry likewise requires re-verification.
    auth.write_auth(email="a@b.com", token="t", verified_at="not-a-date")  # nosec B106
    assert auth.is_verified() is False


def test_is_verified_accepts_epoch_expiry() -> None:
    # A relay expiry expressed as epoch seconds must not be misread as missing.
    future = (datetime.now(UTC) + timedelta(hours=1)).timestamp()
    past = (datetime.now(UTC) - timedelta(hours=1)).timestamp()

    # As a numeric string (how write_auth persists it).
    auth.write_auth(email="a@b.com", token="t", verified_at=str(future))  # nosec B106
    assert auth.is_verified() is True
    auth.write_auth(email="a@b.com", token="t", verified_at=str(past))  # nosec B106
    assert auth.is_verified() is False

    # As a raw JSON number, if a record is written that way.
    auth.AUTH_PATH.parent.mkdir(parents=True, exist_ok=True)
    auth.AUTH_PATH.write_text(
        f'{{"email": "a@b.com", "token": "t", "verified_at": {future}}}', encoding="utf-8"
    )
    assert auth.is_verified() is True


def test_write_auth_is_0600() -> None:
    auth.write_auth(email="a@b.com", token="t", verified_at="")  # nosec B106
    mode = stat.S_IMODE(auth.AUTH_PATH.stat().st_mode)
    assert mode == 0o600


def test_read_auth_rejects_incomplete_record() -> None:
    auth.AUTH_PATH.parent.mkdir(parents=True, exist_ok=True)
    auth.AUTH_PATH.write_text('{"email": "a@b.com"}', encoding="utf-8")
    assert auth.read_auth() is None
    assert auth.is_verified() is False


def _stub_post(monkeypatch: pytest.MonkeyPatch, status: int, body: dict[str, Any]) -> None:
    def fake(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int, dict[str, Any]]:
        return status, body

    monkeypatch.setattr(auth, "_post_json", fake)


def test_otp_start_maps_errors(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub_post(monkeypatch, 200, {"ok": True})
    auth.otp_start("a@b.com")  # no raise

    _stub_post(monkeypatch, 429, {"error": "rate_limited"})
    with pytest.raises(auth.RelayError) as exc:
        auth.otp_start("a@b.com")
    assert exc.value.code == "rate_limited"

    _stub_post(monkeypatch, 400, {})
    with pytest.raises(auth.RelayError) as exc:
        auth.otp_start("bad")
    assert exc.value.code == "invalid_email"


def test_otp_verify_success_and_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
    expires = _iso(timedelta(hours=1))
    _stub_post(monkeypatch, 200, {"token": "t", "email": "a@b.com", "expires_at": expires})
    result = auth.otp_verify("a@b.com", "123456")
    assert result["token"] == "t"

    _stub_post(monkeypatch, 403, {"error": "invalid_code"})
    with pytest.raises(auth.RelayError) as exc:
        auth.otp_verify("a@b.com", "000000")
    assert exc.value.code == "invalid_code"


def test_otp_verify_rejects_token_without_usable_expiry(monkeypatch: pytest.MonkeyPatch) -> None:
    # A 200 with a token but no valid expiry must not be reported as success,
    # otherwise the caller would store a record that immediately reads unverified.
    for expires in (None, "", "later"):
        _stub_post(monkeypatch, 200, {"token": "t", "email": "a@b.com", "expires_at": expires})
        with pytest.raises(auth.RelayError) as exc:
            auth.otp_verify("a@b.com", "123456")
        assert exc.value.code == "unavailable"


def test_report_send_never_includes_password(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}

    def fake(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int, dict[str, Any]]:
        captured["payload"] = payload
        return 200, {"ok": True}

    monkeypatch.setattr(auth, "_post_json", fake)
    auth.report_send("tok", b"%PDF-fake", "strix-report-x.pdf", "x", "https://example.com")

    payload = captured["payload"]
    assert set(payload) == {"token", "pdf_base64", "filename", "run_name", "target"}
    # The password is generated locally and must never appear in the relay body.
    assert "password" not in payload
    assert all("password" not in str(k).lower() for k in payload)


def test_report_send_reverify_on_401(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub_post(monkeypatch, 401, {"error": "invalid_token"})
    with pytest.raises(auth.RelayError) as exc:
        auth.report_send("tok", b"x", "f.pdf", "r", "t")
    assert exc.value.code == "reverify"
