"""Tests for the end-to-end audit pipeline.

Covers OBPI-0.19.0-02, OBPI-0.19.0-04, OBPI-0.19.0-05, and OBPI-0.19.0-06.

Verifies that ``gz audit ADR-X.Y.Z`` emits a validation receipt,
transitions the ADR to Validated when all checks pass, preserves
the attestation guard, enriches AUDIT.md with attestation record,
gate results, and evidence links, and renders from templates with
evidence aggregated from the ledger.
"""

import json
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

from gzkit.cli import _write_audit_artifacts, main
from gzkit.commands.common import aggregate_audit_evidence
from gzkit.ledger import (
    Ledger,
    adr_created_event,
    attested_event,
    closeout_initiated_event,
    gate_checked_event,
    obpi_created_event,
    obpi_receipt_emitted_event,
)
from gzkit.quality import QualityResult
from gzkit.templates import load_template
from gzkit.traceability import covers
from tests.commands.common import (
    CliRunner,
    _init_git_repo,
    _quick_init,
    start_init_subprocess_patches,
    stop_init_subprocess_patches,
)

# Module-level cached templates (GHI #253): most tests in this module start
# from either a bare ``init_git_repo + _quick_init`` state or an attested-ADR
# state. Caching these once and copytree-ing per test saves ~45ms per test.
_TEMPLATE_CTX: tempfile.TemporaryDirectory | None = None
_BARE_TEMPLATE: Path | None = None
_ATTESTED_TEMPLATE: Path | None = None


def setUpModule() -> None:
    global _TEMPLATE_CTX, _BARE_TEMPLATE, _ATTESTED_TEMPLATE
    start_init_subprocess_patches()
    _TEMPLATE_CTX = tempfile.TemporaryDirectory(prefix="gzkit-audit-pl-")
    root = Path(_TEMPLATE_CTX.name)
    _BARE_TEMPLATE = root / "bare"
    _BARE_TEMPLATE.mkdir()
    _ATTESTED_TEMPLATE = root / "attested"
    _ATTESTED_TEMPLATE.mkdir()

    orig = Path.cwd()
    # Build bare template: git init + quick init.
    os.chdir(_BARE_TEMPLATE)
    try:
        _init_git_repo(Path.cwd())
        _quick_init()
    finally:
        os.chdir(orig)

    # Build attested template on top of bare: copytree + plan create + events.
    shutil.copytree(_BARE_TEMPLATE, _ATTESTED_TEMPLATE, dirs_exist_ok=True)
    os.chdir(_ATTESTED_TEMPLATE)
    try:
        CliRunner().invoke(main, ["plan", "create", "f", "--kind", "feature"])
        ledger = Ledger(Path(".gzkit/ledger.jsonl"))
        ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))
        ledger.append(attested_event("ADR-0.1.0-f", "completed", "Test User"))
    finally:
        os.chdir(orig)


def tearDownModule() -> None:
    global _TEMPLATE_CTX, _BARE_TEMPLATE, _ATTESTED_TEMPLATE
    if _TEMPLATE_CTX is not None:
        _TEMPLATE_CTX.cleanup()
    _TEMPLATE_CTX = None
    _BARE_TEMPLATE = None
    _ATTESTED_TEMPLATE = None
    stop_init_subprocess_patches()


class _CopyTemplate:
    """Context manager: copytree a cached template into a fresh tempdir."""

    def __init__(self, template: Path) -> None:
        self._template = template
        self._tmpctx: tempfile.TemporaryDirectory | None = None
        self._orig_cwd: Path | None = None

    def __enter__(self) -> None:
        self._tmpctx = tempfile.TemporaryDirectory(prefix="gzkit-audit-pl-test-")
        dest = Path(self._tmpctx.name) / "project"
        shutil.copytree(self._template, dest)
        self._orig_cwd = Path.cwd()
        os.chdir(dest)

    def __exit__(self, *exc: object) -> None:
        if self._orig_cwd is not None:
            os.chdir(self._orig_cwd)
        if self._tmpctx is not None:
            self._tmpctx.cleanup()


def _bare_workspace() -> _CopyTemplate:
    assert _BARE_TEMPLATE is not None
    return _CopyTemplate(_BARE_TEMPLATE)


def _attested_workspace() -> _CopyTemplate:
    assert _ATTESTED_TEMPLATE is not None
    return _CopyTemplate(_ATTESTED_TEMPLATE)


def _make_qr(success: bool = True, command: str = "test", returncode: int = 0) -> QualityResult:
    return QualityResult(
        success=success,
        command=command,
        stdout="OK" if success else "",
        stderr="" if success else "FAIL",
        returncode=returncode,
    )


class TestAuditAttestationGuard(unittest.TestCase):
    """Audit blocks when ADR is not attested (REQ-01, REQ-09)."""

    @covers("REQ-0.19.0-02-01")
    def test_audit_blocks_without_attestation(self):
        runner = CliRunner()
        with _bare_workspace():
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 1)
            self.assertIn("attestation", result.output.lower())


class TestAuditArtifacts(unittest.TestCase):
    """Audit creates artifacts and runs commands (REQ-02)."""

    @covers("REQ-0.19.0-02-02")
    @patch("gzkit.cli.main.run_command")
    def test_audit_creates_artifacts(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0, result.output)
            adr_dir = next(Path("design/adr").rglob("ADR-0.1.0-f*.md")).parent
            audit_dir = adr_dir / "audit"
            self.assertTrue(audit_dir.exists())
            self.assertTrue((audit_dir / "AUDIT_PLAN.md").exists())
            self.assertTrue((audit_dir / "AUDIT.md").exists())
            self.assertTrue((audit_dir / "proofs").exists())


class TestAuditReceiptEmission(unittest.TestCase):
    """Audit emits a validation receipt to the ledger (REQ-03)."""

    @covers("REQ-0.19.0-02-03")
    @patch("gzkit.cli.main.run_command")
    def test_validation_receipt_in_ledger(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0, result.output)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertIn("audit_receipt_emitted", ledger_text)
            self.assertIn("validated", ledger_text)


class TestAuditStatusTransition(unittest.TestCase):
    """Audit transitions ADR to Validated when all pass (REQ-04)."""

    @covers("REQ-0.19.0-02-04")
    @covers("REQ-0.19.0-07-01")
    @patch("gzkit.cli.main.run_command")
    def test_adr_transitions_to_validated(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0, result.output)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertIn("lifecycle_transition", ledger_text)
            self.assertIn("Validated", ledger_text)


class TestAuditFailureNoTransition(unittest.TestCase):
    """Audit does NOT transition ADR on failure (REQ-05, REQ-08)."""

    @covers("REQ-0.19.0-02-05")
    @patch("gzkit.cli.main.run_command")
    def test_no_transition_on_failure(self, mock_run):
        mock_run.return_value = _make_qr(success=False, returncode=1)
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 1)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            # Receipt should still be emitted (with failure recorded)
            self.assertIn("audit_receipt_emitted", ledger_text)
            # But no lifecycle transition
            self.assertNotIn("lifecycle_transition", ledger_text)

    @covers("REQ-0.19.0-07-02")
    @covers("REQ-0.19.0-07-05")
    @patch("gzkit.cli.main.run_command")
    def test_artifacts_still_written_on_failure(self, mock_run):
        mock_run.return_value = _make_qr(success=False, returncode=1)
        runner = CliRunner()
        with _attested_workspace():
            runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            adr_dir = next(Path("design/adr").rglob("ADR-0.1.0-f*.md")).parent
            self.assertTrue((adr_dir / "audit" / "AUDIT.md").exists())


class TestAuditDryRun(unittest.TestCase):
    """--dry-run shows full plan without executing (REQ-06)."""

    @covers("REQ-0.19.0-02-06")
    def test_dry_run_shows_receipt_and_transition_plan(self):
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--dry-run"])
            self.assertEqual(result.exit_code, 0, result.output)
            self.assertIn("Dry run", result.output)
            self.assertIn("receipt", result.output.lower())
            self.assertIn("Validated", result.output)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertNotIn("audit_receipt_emitted", ledger_text)

    @covers("REQ-0.19.0-07-03")
    def test_dry_run_json_includes_receipt_and_transition(self):
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--dry-run", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            self.assertIn("validation_receipt", data)
            self.assertIn("status_transition", data)


class TestAuditJsonOutput(unittest.TestCase):
    """--json emits structured output with all stages (REQ-07)."""

    @covers("REQ-0.19.0-02-07")
    @patch("gzkit.cli.main.run_command")
    def test_json_contains_all_fields(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            self.assertIn("results", data)
            self.assertIn("validation_receipt", data)
            self.assertIn("status_transition", data)
            self.assertTrue(data["passed"])


# ---------------------------------------------------------------------------
# OBPI-0.19.0-04: Enrichment tests (attestation record, gate results, evidence links)
# ---------------------------------------------------------------------------


def _make_ledger_with_events(ledger_path: Path, adr_id: str) -> Ledger:
    """Create a ledger populated with attested and gate_checked events."""
    ledger = Ledger(ledger_path)
    ledger.append(gate_checked_event(adr_id, 2, "pass", "uv run gz test", 0))
    ledger.append(gate_checked_event(adr_id, 5, "pass", "uv run gz lint", 0))
    ledger.append(attested_event(adr_id, "completed", "human:Jeff"))
    return ledger


def _call_write_audit_artifacts(
    tmp: Path,
    adr_id: str = "ADR-0.19.0-f",
    ledger: Ledger | None = None,
    result_rows: list | None = None,
    create_obpi_files: bool = False,
    create_closeout_form: bool = False,
) -> tuple[Path, Path, dict]:
    """Set up directory structure and call _write_audit_artifacts."""
    project_root = tmp
    adr_dir = tmp / "docs" / "design" / "adr" / "ADR-0.19.0-test"
    adr_file = adr_dir / f"{adr_id}-some-title.md"
    adr_dir.mkdir(parents=True, exist_ok=True)
    adr_file.write_text("# ADR\n", encoding="utf-8")

    if create_obpi_files:
        obpi_dir = adr_dir / "obpis"
        obpi_dir.mkdir(parents=True, exist_ok=True)
        (obpi_dir / "OBPI-0.19.0-01-first.md").write_text("# OBPI\n", encoding="utf-8")
        (obpi_dir / "OBPI-0.19.0-02-second.md").write_text("# OBPI\n", encoding="utf-8")

    if create_closeout_form:
        closeout_dir = adr_dir / "closeout"
        closeout_dir.mkdir(parents=True, exist_ok=True)
        (closeout_dir / "CLOSEOUT.md").write_text("# Closeout\n", encoding="utf-8")

    audit_dir = adr_dir / "audit"
    proofs_dir = audit_dir / "proofs"
    proofs_dir.mkdir(parents=True, exist_ok=True)

    if result_rows is None:
        result_rows = [
            {
                "label": "Lint",
                "command": "uv run gz lint",
                "success": True,
                "returncode": 0,
                "proof_file": "design/adr/ADR-0.19.0-test/audit/proofs/lint.txt",
            }
        ]

    plan_file, audit_file, enrichment = _write_audit_artifacts(
        adr_id=adr_id,
        adr_file=adr_file,
        audit_dir=audit_dir,
        proofs_dir=proofs_dir,
        result_rows=result_rows,
        project_root=project_root,
        ledger=ledger,
    )
    return plan_file, audit_file, enrichment


class TestAuditMdAttestationRecord(unittest.TestCase):
    """REQ-01: AUDIT.md must contain an Attestation Record section."""

    @covers("REQ-0.19.0-04-01")
    def test_attestation_section_rendered_with_event(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_events(ledger_path, "ADR-0.19.0-f")
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Attestation Record", content)
            self.assertIn("human:Jeff", content)
            self.assertIn("completed", content)

    def test_attestation_section_fallback_when_no_event(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)  # empty ledger
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Attestation Record", content)
            self.assertIn("No attestation record found", content)


class TestAuditMdGateResults(unittest.TestCase):
    """REQ-02: AUDIT.md must contain a Gate Results section."""

    @covers("REQ-0.19.0-04-02")
    def test_gate_results_table_rendered(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_events(ledger_path, "ADR-0.19.0-f")
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Gate Results", content)
            self.assertIn("| Gate |", content)
            self.assertIn("uv run gz test", content)
            self.assertIn("uv run gz lint", content)

    @covers("REQ-0.19.0-04-05")
    def test_gate_results_note_when_no_events(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)  # no gate events
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Gate Results", content)
            self.assertIn("No prior gate results recorded", content)


class TestAuditMdEvidenceLinks(unittest.TestCase):
    """REQ-03: AUDIT.md must contain an Evidence Links section."""

    @covers("REQ-0.19.0-04-03")
    def test_obpi_files_listed(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            _, audit_file, _ = _call_write_audit_artifacts(
                tmp, ledger=ledger, create_obpi_files=True
            )
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Evidence Links", content)
            self.assertIn("OBPI-0.19.0-01-first.md", content)
            self.assertIn("OBPI-0.19.0-02-second.md", content)

    def test_closeout_form_listed_when_present(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            _, audit_file, _ = _call_write_audit_artifacts(
                tmp, ledger=ledger, create_obpi_files=True, create_closeout_form=True
            )
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Evidence Links", content)
            self.assertIn("Closeout:", content)
            self.assertIn("CLOSEOUT.md", content)

    def test_evidence_links_use_forward_slashes(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            _, audit_file, _ = _call_write_audit_artifacts(
                tmp, ledger=ledger, create_obpi_files=True
            )
            content = audit_file.read_text(encoding="utf-8")
            # Paths in the Evidence Links section must use forward slashes
            self.assertNotIn("\\", content)

    def test_no_evidence_links_note_when_empty(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Evidence Links", content)
            self.assertIn("No evidence links found", content)


class TestAuditEnrichmentJsonKeys(unittest.TestCase):
    """REQ-04: JSON output must contain enrichment keys alongside existing keys."""

    @patch("gzkit.cli.main.run_command")
    def test_json_output_contains_enrichment_keys(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            self.assertIn("attestation_record", data)
            self.assertIn("gate_results", data)
            self.assertIn("evidence_links", data)

    @covers("REQ-0.19.0-04-04")
    @patch("gzkit.cli.main.run_command")
    def test_json_output_preserves_existing_keys(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            # Existing keys must be preserved unchanged
            self.assertIn("adr", data)
            self.assertIn("audit_file", data)
            self.assertIn("audit_plan_file", data)
            self.assertIn("results", data)
            self.assertIn("passed", data)

    @patch("gzkit.cli.main.run_command")
    def test_attestation_record_has_correct_fields(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            rec = data["attestation_record"]
            self.assertIn("attestor", rec)
            self.assertIn("status", rec)
            self.assertIn("timestamp", rec)
            self.assertEqual(rec["attestor"], "Test User")
            self.assertEqual(rec["status"], "completed")

    @patch("gzkit.cli.main.run_command")
    def test_gate_results_list_has_correct_structure(self, mock_run):
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0, result.output)
            data = json.loads(result.output)
            gate_results = data["gate_results"]
            self.assertIsInstance(gate_results, list)
            self.assertGreaterEqual(len(gate_results), 1)
            first = gate_results[0]
            self.assertIn("gate", first)
            self.assertIn("status", first)
            self.assertIn("command", first)
            self.assertIn("returncode", first)


class TestAuditGeneratedLedgerEvent(unittest.TestCase):
    """audit_cmd() appends audit_generated event to ledger (OBPI-0.19.0-05)."""

    @covers("REQ-0.19.0-05-01")
    @covers("REQ-0.19.0-05-02")
    @patch("gzkit.cli.main.run_command")
    def test_audit_generated_event_in_ledger(self, mock_run):
        """Successful audit appends audit_generated with correct fields."""
        mock_run.return_value = _make_qr()
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0, result.output)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertIn('"audit_generated"', ledger_text)
            # Parse the specific event
            events = [json.loads(line) for line in ledger_text.strip().splitlines()]
            audit_events = [e for e in events if e["event"] == "audit_generated"]
            self.assertEqual(len(audit_events), 1)
            evt = audit_events[0]
            self.assertEqual(evt["id"], "ADR-0.1.0-f")
            self.assertIn("AUDIT.md", evt["audit_file"])
            self.assertIn("AUDIT_PLAN.md", evt["audit_plan_file"])
            self.assertTrue(evt["passed"])

    @covers("REQ-0.19.0-05-05")
    @patch("gzkit.cli.main.run_command")
    def test_audit_generated_passed_false_on_failure(self, mock_run):
        """Failed verification records passed=False in audit_generated event."""
        mock_run.return_value = _make_qr(success=False, returncode=1)
        runner = CliRunner()
        with _attested_workspace():
            runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            events = [json.loads(line) for line in ledger_text.strip().splitlines()]
            audit_events = [e for e in events if e["event"] == "audit_generated"]
            self.assertEqual(len(audit_events), 1)
            self.assertFalse(audit_events[0]["passed"])

    @covers("REQ-0.19.0-05-03")
    def test_dry_run_no_audit_generated_event(self):
        """--dry-run does NOT append audit_generated event."""
        runner = CliRunner()
        with _attested_workspace():
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f", "--dry-run"])
            self.assertEqual(result.exit_code, 0, result.output)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertNotIn("audit_generated", ledger_text)

    @covers("REQ-0.19.0-05-04")
    def test_attestation_blocker_no_audit_generated_event(self):
        """Attestation blocker (exit 1) does NOT append audit_generated event."""
        runner = CliRunner()
        with _bare_workspace():
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            # No attestation — should block
            result = runner.invoke(main, ["audit", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 1)
            ledger_text = Path(".gzkit/ledger.jsonl").read_text(encoding="utf-8")
            self.assertNotIn("audit_generated", ledger_text)


# ---------------------------------------------------------------------------
# OBPI-0.19.0-06: Audit templates and evidence aggregation from ledger
# ---------------------------------------------------------------------------


class TestAuditTemplatesExist(unittest.TestCase):
    """REQ-0.19.0-06-01: audit.md and audit_plan.md templates exist with .format() placeholders."""

    @covers("REQ-0.19.0-06-01")
    def test_audit_template_exists_and_has_placeholders(self):
        content = load_template("audit")
        for placeholder in (
            "{adr_id}",
            "{adr_path}",
            "{attestation_section}",
            "{gate_results_section}",
            "{obpi_summary_section}",
            "{verification_results_section}",
            "{evidence_links_section}",
        ):
            self.assertIn(placeholder, content, f"Missing placeholder: {placeholder}")

    def test_audit_plan_template_exists_and_has_placeholders(self):
        content = load_template("audit_plan")
        for placeholder in (
            "{adr_id}",
            "{adr_path}",
            "{verification_commands_section}",
            "{proofs_dir}",
        ):
            self.assertIn(placeholder, content, f"Missing placeholder: {placeholder}")

    def test_templates_use_format_not_jinja(self):
        """Templates must NOT use Jinja2 syntax (REQ-06: NEVER)."""
        for name in ("audit", "audit_plan"):
            content = load_template(name)
            self.assertNotIn("{%", content, f"{name} template contains Jinja2 block tag")
            self.assertNotIn("{{", content, f"{name} template contains Jinja2 variable tag")


def _make_ledger_with_obpis(ledger_path: Path, adr_id: str) -> Ledger:
    """Create a ledger with ADR, child OBPIs, gate checks, attestation, and closeout."""
    ledger = Ledger(ledger_path)
    ledger.append(adr_created_event(adr_id, "lite", "test-slug"))
    for i in range(1, 4):
        obpi_id = f"OBPI-0.19.0-{i:02d}"
        ledger.append(obpi_created_event(obpi_id, adr_id))
        if i <= 2:
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id,
                    receipt_event="completed",
                    attestor="agent",
                    parent_adr=adr_id,
                    obpi_completion="completed",
                )
            )
    ledger.append(gate_checked_event(adr_id, 2, "pass", "uv run gz test", 0))
    ledger.append(gate_checked_event(adr_id, 5, "pass", "uv run gz lint", 0))
    ledger.append(attested_event(adr_id, "completed", "human:Jeff"))
    ledger.append(closeout_initiated_event(adr_id, by="agent", mode="standard"))
    return ledger


class TestAggregateAuditEvidence(unittest.TestCase):
    """REQ-0.19.0-06-02 / REQ-0.19.0-06-04 / REQ-0.19.0-06-05:
    aggregate_audit_evidence returns correct keys, includes OBPI completions,
    and is deterministic.
    """

    @covers("REQ-0.19.0-06-02")
    def test_returns_all_required_keys(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            self.assertIn("obpi_completions", result)
            self.assertIn("gate_results", result)
            self.assertIn("attestation", result)
            self.assertIn("closeout", result)

    @covers("REQ-0.19.0-06-04")
    def test_obpi_completions_lists_all_children(self):
        """REQ-04: Three OBPIs, two completed."""
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            completions = result["obpi_completions"]
            self.assertEqual(len(completions), 3)
            completed_ids = [c["obpi_id"] for c in completions if c["ledger_completed"]]
            self.assertEqual(len(completed_ids), 2)
            # Each entry has required fields
            for c in completions:
                self.assertIn("obpi_id", c)
                self.assertIn("receipt_event", c)
                self.assertIn("ledger_completed", c)

    def test_gate_results_structure(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            self.assertEqual(len(result["gate_results"]), 2)
            for gr in result["gate_results"]:
                self.assertIn("gate", gr)
                self.assertIn("status", gr)
                self.assertIn("command", gr)
                self.assertIn("returncode", gr)

    def test_attestation_populated(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            att = result["attestation"]
            self.assertIsNotNone(att)
            self.assertEqual(att["by"], "human:Jeff")
            self.assertEqual(att["status"], "completed")
            self.assertIn("ts", att)

    def test_closeout_populated(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            co = result["closeout"]
            self.assertIsNotNone(co)
            self.assertEqual(co["by"], "agent")
            self.assertEqual(co["mode"], "standard")

    @covers("REQ-0.19.0-06-05")
    def test_deterministic_ordering(self):
        """REQ-05: Same ledger state produces identical results."""
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            graph = ledger.get_artifact_graph()
            result1 = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            result2 = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            self.assertEqual(result1, result2)

    def test_empty_ledger_returns_defaults(self):
        with tempfile.TemporaryDirectory() as tmp_str:
            ledger_path = Path(tmp_str) / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            graph = ledger.get_artifact_graph()
            result = aggregate_audit_evidence(ledger, "ADR-0.19.0-f", graph)
            self.assertEqual(result["obpi_completions"], [])
            self.assertEqual(result["gate_results"], [])
            self.assertIsNone(result["attestation"])
            self.assertIsNone(result["closeout"])


class TestAuditMdRenderedFromTemplate(unittest.TestCase):
    """REQ-0.19.0-06-03: AUDIT.md is rendered from template, not inline assembly."""

    @covers("REQ-0.19.0-06-03")
    def test_audit_md_contains_obpi_summary_section(self):
        """Rendered AUDIT.md includes the OBPI Completion Summary section."""
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_obpis(ledger_path, "ADR-0.19.0-f")
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## OBPI Completion Summary", content)
            self.assertIn("OBPI-0.19.0-01", content)
            self.assertIn("OBPI-0.19.0-02", content)

    def test_audit_md_contains_generated_timestamp(self):
        """Rendered AUDIT.md includes a Generated timestamp from template."""
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_events(ledger_path, "ADR-0.19.0-f")
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("Generated:", content)

    def test_audit_plan_md_rendered_from_template(self):
        """AUDIT_PLAN.md is rendered from audit_plan template."""
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = Ledger(ledger_path)
            plan_file, _, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = plan_file.read_text(encoding="utf-8")
            self.assertIn("# Audit Plan: ADR-0.19.0-f", content)
            self.assertIn("Generated:", content)
            self.assertIn("uv run gz lint", content)

    def test_audit_md_verification_results_section(self):
        """Rendered AUDIT.md has Verification Results section (not old Results)."""
        with tempfile.TemporaryDirectory() as tmp_str:
            tmp = Path(tmp_str)
            ledger_path = tmp / ".gzkit" / "ledger.jsonl"
            ledger_path.parent.mkdir(parents=True, exist_ok=True)
            ledger = _make_ledger_with_events(ledger_path, "ADR-0.19.0-f")
            _, audit_file, _ = _call_write_audit_artifacts(tmp, ledger=ledger)
            content = audit_file.read_text(encoding="utf-8")
            self.assertIn("## Verification Results", content)
            self.assertIn("PASS", content)
