"""Tests for the optional promotion-readiness gate."""
from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import patch

import pytest

from abi_evals.promotion_gate import (
    CANDIDATE_FILENAME,
    RECEIPT_FILENAME,
    PromotionGateResult,
    evaluate_promotion,
    find_candidate,
)

FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def evidence_with_candidate() -> Path:
    """Evidence directory that contains a promotion_candidate.json."""
    return FIXTURES_DIR / "evidence_with_candidate"


@pytest.fixture
def evidence_without_candidate(tmp_path: Path) -> Path:
    """Evidence directory with no candidate file."""
    (tmp_path / "manifest.json").write_text("{}")
    return tmp_path


@pytest.fixture
def sample_receipt() -> dict:
    """A minimal valid promotion receipt for mocking."""
    return {
        "receipt_version": "1.0",
        "candidate_hash": "a" * 64,
        "checks": [
            {
                "check": "sha256_hex_length",
                "artifact_path": "artifacts/service-spec.json",
                "result": "pass",
            },
        ],
        "status": "pass",
        "violations": [],
        "receipt_hash": "b" * 64,
    }


# ---------------------------------------------------------------------------
# find_candidate
# ---------------------------------------------------------------------------

class TestFindCandidate:
    def test_present(self, evidence_with_candidate: Path):
        result = find_candidate(evidence_with_candidate)
        assert result is not None
        assert result.name == CANDIDATE_FILENAME

    def test_absent(self, evidence_without_candidate: Path):
        result = find_candidate(evidence_without_candidate)
        assert result is None


# ---------------------------------------------------------------------------
# evaluate_promotion — clean skip
# ---------------------------------------------------------------------------

class TestEvaluatePromotionSkip:
    def test_skips_when_no_candidate(self, evidence_without_candidate: Path):
        result = evaluate_promotion(evidence_without_candidate)
        assert result.status == "skipped"
        assert "skipping" in result.message.lower()
        assert result.receipt is None
        assert result.receipt_path is None

    def test_skip_summary_dict(self, evidence_without_candidate: Path):
        result = evaluate_promotion(evidence_without_candidate)
        d = result.to_dict()
        assert d["promotion"]["status"] == "skipped"


# ---------------------------------------------------------------------------
# evaluate_promotion — candidate present (mocked subprocess)
# ---------------------------------------------------------------------------

class TestEvaluatePromotionWithCandidate:
    def test_produces_receipt_artifact(
        self,
        evidence_with_candidate: Path,
        sample_receipt: dict,
        tmp_path: Path,
    ):
        """When verify-candidate succeeds, receipt is stored and summary returned."""
        output_dir = tmp_path / "eval_output"

        def fake_run(cmd, *, capture_output, text):
            """Write a receipt file and return success."""
            out_dir = Path(cmd[cmd.index("--out") + 1])
            out_dir.mkdir(parents=True, exist_ok=True)
            receipt_path = out_dir / RECEIPT_FILENAME
            receipt_path.write_text(
                json.dumps(sample_receipt, indent=2) + "\n",
                encoding="utf-8",
            )

            class _Result:
                returncode = 0
                stdout = f"VERIFY-CANDIDATE: {sample_receipt['status']}\n"
                stderr = ""

            return _Result()

        with patch("abi_evals.promotion_gate.subprocess.run", side_effect=fake_run):
            result = evaluate_promotion(
                evidence_with_candidate,
                output_dir=output_dir,
            )

        assert result.status == "pass"
        assert result.receipt is not None
        assert result.receipt["receipt_version"] == "1.0"
        assert result.receipt_path is not None

        # Receipt file should exist on disk
        assert Path(result.receipt_path).is_file()

    def test_summary_dict_structure(
        self,
        evidence_with_candidate: Path,
        sample_receipt: dict,
        tmp_path: Path,
    ):
        """to_dict() includes receipt_summary with expected keys."""
        output_dir = tmp_path / "eval_output"

        def fake_run(cmd, *, capture_output, text):
            out_dir = Path(cmd[cmd.index("--out") + 1])
            out_dir.mkdir(parents=True, exist_ok=True)
            (out_dir / RECEIPT_FILENAME).write_text(
                json.dumps(sample_receipt, indent=2) + "\n",
                encoding="utf-8",
            )

            class _Result:
                returncode = 0
                stdout = ""
                stderr = ""

            return _Result()

        with patch("abi_evals.promotion_gate.subprocess.run", side_effect=fake_run):
            result = evaluate_promotion(evidence_with_candidate, output_dir=output_dir)

        d = result.to_dict()
        promo = d["promotion"]
        assert promo["status"] == "pass"
        assert "receipt_summary" in promo
        assert promo["receipt_summary"]["receipt_version"] == "1.0"
        assert promo["receipt_summary"]["checks_count"] == 1
        assert promo["receipt_summary"]["violations_count"] == 0

    def test_warn_status_propagated(
        self,
        evidence_with_candidate: Path,
        sample_receipt: dict,
        tmp_path: Path,
    ):
        """A 'warn' status from the receipt flows through to the result."""
        output_dir = tmp_path / "eval_output"
        warn_receipt = {**sample_receipt, "status": "warn"}

        def fake_run(cmd, *, capture_output, text):
            out_dir = Path(cmd[cmd.index("--out") + 1])
            out_dir.mkdir(parents=True, exist_ok=True)
            (out_dir / RECEIPT_FILENAME).write_text(
                json.dumps(warn_receipt, indent=2) + "\n",
                encoding="utf-8",
            )

            class _Result:
                returncode = 0
                stdout = ""
                stderr = ""

            return _Result()

        with patch("abi_evals.promotion_gate.subprocess.run", side_effect=fake_run):
            result = evaluate_promotion(evidence_with_candidate, output_dir=output_dir)

        assert result.status == "warn"

    def test_fail_status_propagated(
        self,
        evidence_with_candidate: Path,
        sample_receipt: dict,
        tmp_path: Path,
    ):
        """A 'fail' status from the receipt flows through to the result."""
        output_dir = tmp_path / "eval_output"
        fail_receipt = {**sample_receipt, "status": "fail"}

        def fake_run(cmd, *, capture_output, text):
            out_dir = Path(cmd[cmd.index("--out") + 1])
            out_dir.mkdir(parents=True, exist_ok=True)
            (out_dir / RECEIPT_FILENAME).write_text(
                json.dumps(fail_receipt, indent=2) + "\n",
                encoding="utf-8",
            )

            class _Result:
                returncode = 0
                stdout = ""
                stderr = ""

            return _Result()

        with patch("abi_evals.promotion_gate.subprocess.run", side_effect=fake_run):
            result = evaluate_promotion(evidence_with_candidate, output_dir=output_dir)

        assert result.status == "fail"


# ---------------------------------------------------------------------------
# evaluate_promotion — error paths
# ---------------------------------------------------------------------------

class TestEvaluatePromotionErrors:
    def test_missing_abi_promotion(self, evidence_with_candidate: Path, tmp_path: Path):
        """When abi-promotion is not installed, result is 'error'."""
        with patch(
            "abi_evals.promotion_gate.subprocess.run",
            side_effect=FileNotFoundError("not found"),
        ):
            result = evaluate_promotion(evidence_with_candidate, output_dir=tmp_path)

        assert result.status == "error"
        assert "not installed" in result.message.lower() or "PATH" in result.message

    def test_no_receipt_produced(self, evidence_with_candidate: Path, tmp_path: Path):
        """When subprocess runs but produces no receipt file."""

        class _Result:
            returncode = 1
            stdout = ""
            stderr = "jsonschema.ValidationError: bad input"

        with patch(
            "abi_evals.promotion_gate.subprocess.run",
            return_value=_Result(),
        ):
            result = evaluate_promotion(evidence_with_candidate, output_dir=tmp_path)

        assert result.status == "error"
        assert "no receipt" in result.message.lower()


# ---------------------------------------------------------------------------
# PromotionGateResult.to_dict
# ---------------------------------------------------------------------------

class TestPromotionGateResultDict:
    def test_minimal_skip(self):
        r = PromotionGateResult(status="skipped", message="No candidate found.")
        d = r.to_dict()
        assert d == {"promotion": {"status": "skipped", "message": "No candidate found."}}

    def test_full_result(self, sample_receipt: dict):
        r = PromotionGateResult(
            status="pass",
            receipt_path="/tmp/receipt.json",
            message="ok",
            receipt=sample_receipt,
        )
        d = r.to_dict()
        assert d["promotion"]["receipt_path"] == "/tmp/receipt.json"
        assert d["promotion"]["receipt_summary"]["status"] == "pass"
