# -*- coding: utf-8 -*-
"""tests.regression.test_owner_gemini_trigger_router_fixture_parametrized — task-2641 Track A.

회장 verbatim §12 (2026-05-23) 1:1 정합 — OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE
재발 방지 fixture 6 시나리오 parametrized PASS 회귀.

본 회귀는 Layer A / NO-CRON: subprocess / cokacdir / merge / cron / live gh 호출 0.
모든 시나리오는 fixtures/owner_gemini_trigger_router/<scenario>/evidence.json 의 입력으로
router 를 호출하고 expected.json 의 final_state / freshness_state / nudge_attempted
/ permission_header_diagnostics whitelist + redact / token_value_logged=False / audit
schema 단언.
"""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from anu_v2.gemini_evidence_freshness_checker import (
    check_gemini_evidence_fresh,
)
from anu_v2.owner_gemini_trigger_router import (
    OwnerGeminiTriggerRouter,
)
from anu_v2.owner_gemini_trigger_router_audit import (
    AUDIT_SCHEMA,
    OwnerGeminiTriggerRouterAudit,
)


WORKSPACE = Path(__file__).resolve().parent.parent.parent
FIXTURE_ROOT = WORKSPACE / "tests" / "fixtures" / "owner_gemini_trigger_router"

SCENARIOS = (
    "pr_review_empty_body_misroute_block",
    "issue_comment_exact_body_trigger_success",
    "permission_403_diagnostics_record",
    "nudge_limit_exceeded_dedupe",
    "fresh_review_arrives_within_timeout",
    "stale_after_nudge_timeout_classify",
)


def _load_fixture(scenario: str) -> tuple[dict, dict]:
    fdir = FIXTURE_ROOT / scenario
    evidence = json.loads((fdir / "evidence.json").read_text(encoding="utf-8"))
    expected = json.loads((fdir / "expected.json").read_text(encoding="utf-8"))
    return evidence, expected


def _build_github_api_for(evidence: dict):
    """github_api mock — 호출 횟수에 따라 pre/post nudge payload 분리."""
    initial = evidence.get("github_api_payload", [])
    post_payload = evidence.get("post_nudge_github_api_payload")
    counter = {"calls": 0}

    def _api(_method: str, _path: str) -> Any:
        idx = counter["calls"]
        counter["calls"] = idx + 1
        if idx == 0 or post_payload is None:
            return initial
        return post_payload

    return _api


def _make_invoke(evidence: dict):
    status = evidence.get("invoke_status_simulated")
    captured = []

    def _invoke(**kwargs):
        captured.append(kwargs)
        return status

    _invoke.captured = captured  # type: ignore[attr-defined]
    return _invoke


def _make_diag_provider(evidence: dict):
    payload = evidence.get("permission_diagnostics_simulated")

    def _provider():
        return payload

    return _provider


def _seed_prior_audit_records(audit: OwnerGeminiTriggerRouterAudit, records: list):
    for rec in records:
        # required schema fields default-fill (token_value_logged=False enforced).
        rec_to_write = dict(rec)
        rec_to_write.setdefault("token_present", False)
        rec_to_write.setdefault("token_hash_prefix", "")
        rec_to_write.setdefault("permission_header_diagnostics", None)
        rec_to_write.setdefault("nudge_attempted", False)
        audit.append(rec_to_write)


@pytest.mark.parametrize("scenario", SCENARIOS)
def test_fixture_scenario_runs_router_state_machine_to_expected_final_state(
    tmp_path, scenario
):
    evidence, expected = _load_fixture(scenario)

    audit = OwnerGeminiTriggerRouterAudit(tmp_path)
    if evidence.get("prior_audit_records"):
        _seed_prior_audit_records(audit, evidence["prior_audit_records"])

    invoke = _make_invoke(evidence)
    github_api = _build_github_api_for(evidence)
    diag_provider = _make_diag_provider(evidence)

    router = OwnerGeminiTriggerRouter(
        workspace_root=tmp_path,
        freshness_checker=check_gemini_evidence_fresh,
        invoke_scheduler=invoke,
        permission_diagnostics_provider=diag_provider,
        audit=audit,
        github_api=github_api,
    )

    decision_path = "decision.json" if evidence.get("decision_path_present") else None

    result = router.route_for_pr(
        pr_number=evidence["pr_number"],
        current_head_sha=evidence["current_head_sha"],
        owner=evidence["owner"],
        repo=evidence["repo"],
        task_id=evidence.get("task_id", ""),
        decision_path=decision_path,
        observed_comment=evidence.get("observed_comment"),
        fresh_review_arrived_post_nudge=bool(
            evidence.get("fresh_review_arrived_post_nudge", False)
        ),
    )

    # final_state matches expected
    assert result.final_state == expected["final_state"], (
        f"[{scenario}] expected final_state={expected['final_state']!r}, "
        f"got {result.final_state!r}; reason={result.reason!r}"
    )

    # freshness_state matches when not N/A
    if expected.get("freshness_state") != "N/A":
        assert result.freshness_state == expected["freshness_state"], (
            f"[{scenario}] freshness_state mismatch: "
            f"expected={expected['freshness_state']!r}, "
            f"got={result.freshness_state!r}"
        )

    # nudge_attempted boolean exact match
    assert result.nudge_attempted is expected["nudge_attempted"], (
        f"[{scenario}] nudge_attempted mismatch: "
        f"expected={expected['nudge_attempted']}, "
        f"got={result.nudge_attempted}"
    )

    # nudge_result matches
    assert result.nudge_result == expected["nudge_result"], (
        f"[{scenario}] nudge_result mismatch: "
        f"expected={expected['nudge_result']!r}, got={result.nudge_result!r}"
    )

    # gemini_commit_id_observed matches when explicit
    if "gemini_commit_id_observed" in expected:
        assert (
            result.gemini_commit_id_observed
            == expected["gemini_commit_id_observed"]
        ), (
            f"[{scenario}] gemini_commit_id_observed mismatch: "
            f"expected={expected['gemini_commit_id_observed']!r}, "
            f"got={result.gemini_commit_id_observed!r}"
        )

    # 회장 verbatim §6: token_value_logged must be False everywhere
    audit_text = audit.path.read_text(encoding="utf-8")
    last_record = json.loads(audit_text.strip().splitlines()[-1])
    assert last_record["schema"] == AUDIT_SCHEMA
    assert last_record["token_value_logged"] is False
    assert last_record["final_state"] == expected["final_state"]

    # reason_must_contain assertions
    for needle in expected.get("reason_must_contain", []):
        assert needle in result.reason, (
            f"[{scenario}] reason {result.reason!r} missing needle {needle!r}"
        )

    # permission_header_diagnostics whitelist + redaction (회장 verbatim §8)
    must_contain_keys = expected.get(
        "permission_header_diagnostics_must_contain_keys"
    )
    if must_contain_keys is not None:
        diag = result.permission_header_diagnostics
        assert isinstance(diag, dict), (
            f"[{scenario}] permission_header_diagnostics must be dict"
        )
        for key in must_contain_keys:
            assert key in diag, (
                f"[{scenario}] missing 403 header key {key!r} — got {sorted(diag)}"
            )

    must_not_contain_keys = expected.get(
        "permission_header_diagnostics_must_not_contain_keys"
    )
    if must_not_contain_keys is not None:
        diag = result.permission_header_diagnostics or {}
        for key in must_not_contain_keys:
            assert key not in diag, (
                f"[{scenario}] unexpected 403 header key {key!r}"
            )

    must_not_contain_values = expected.get(
        "permission_header_diagnostics_must_not_contain_values"
    )
    if must_not_contain_values:
        diag = result.permission_header_diagnostics or {}
        serialised = json.dumps(diag)
        for needle in must_not_contain_values:
            assert needle not in serialised, (
                f"[{scenario}] raw token sentinel {needle!r} leaked into diagnostics"
            )
            # defense in depth — same sentinel must not be in audit jsonl
            assert needle not in audit_text, (
                f"[{scenario}] raw token sentinel {needle!r} leaked into audit"
            )
