"""Tests for the @covers decorator, linkage registry, and coverage anchor scanner.

Validates REQ format checking, brief-backed existence validation,
LinkageRecord registration, metadata-only behavior, AST scanning,
and multi-level coverage rollup computation.
"""

import tempfile
import textwrap
import unittest
from pathlib import Path

from gzkit.traceability import (
    CoverageReport,
    compute_coverage,
    covers,
    get_registry,
    reset_registry,
    scan_test_tree,
    set_known_reqs,
)
from gzkit.triangle import (
    DiscoveredReq,
    EdgeType,
    ReqEntity,
    ReqId,
    ReqStatus,
    VertexType,
)

# Known REQs for testing (simulates brief-extracted REQs)
_TEST_REQS = frozenset(
    {
        "REQ-0.15.0-03-01",
        "REQ-0.15.0-03-02",
        "REQ-0.20.0-01-01",
    }
)


# ---------------------------------------------------------------------------
# OBPI-01: @covers decorator tests
# ---------------------------------------------------------------------------


class TestCoversFormatValidation(unittest.TestCase):
    """REQ format validation at decoration time."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    def test_valid_req_format_accepted(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            return 42

        self.assertEqual(test_fn(), 42)

    @covers("REQ-0.21.0-01-02")
    def test_invalid_format_raises_value_error(self):
        with self.assertRaises(ValueError) as ctx:

            @covers("INVALID")
            def test_fn():
                pass

        self.assertIn("Invalid REQ identifier", str(ctx.exception))

    def test_empty_string_raises_value_error(self):
        with self.assertRaises(ValueError):

            @covers("")
            def test_fn():
                pass

    def test_partial_format_raises_value_error(self):
        with self.assertRaises(ValueError):

            @covers("REQ-0.15.0")
            def test_fn():
                pass

    def test_obpi_format_raises_value_error(self):
        with self.assertRaises(ValueError):

            @covers("OBPI-0.15.0-03")
            def test_fn():
                pass


class TestCoversExistenceValidation(unittest.TestCase):
    """Brief-backed REQ existence validation."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    @covers("REQ-0.21.0-01-03")
    def test_unknown_req_raises_value_error(self):
        with self.assertRaises(ValueError) as ctx:

            @covers("REQ-9.9.9-99-99")
            def test_fn():
                pass

        self.assertIn("Unknown REQ identifier", str(ctx.exception))
        self.assertIn("REQ-9.9.9-99-99", str(ctx.exception))

    def test_known_req_does_not_raise(self):
        @covers("REQ-0.15.0-03-01")
        def test_fn():
            pass

    def test_no_orphan_linkage_on_unknown(self):
        try:

            @covers("REQ-9.9.9-99-99")
            def test_fn():
                pass
        except ValueError:
            pass

        self.assertEqual(len(get_registry()), 0)


class TestCoversLinkageRegistration(unittest.TestCase):
    """LinkageRecord registration and structure."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    @covers("REQ-0.21.0-01-01")
    def test_single_covers_registers_one_linkage(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        self.assertEqual(len(get_registry()), 1)

    def test_linkage_record_structure(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        record = get_registry()[0]
        self.assertEqual(record.edge_type, EdgeType.COVERS)
        self.assertEqual(record.source.vertex_type, VertexType.TEST)
        self.assertEqual(record.target.vertex_type, VertexType.SPEC)
        self.assertEqual(record.target.identifier, "REQ-0.15.0-03-02")
        self.assertIn("test_fn", record.source.identifier)

    @covers("REQ-0.21.0-01-04")
    def test_multiple_covers_register_multiple_linkages(self):
        @covers("REQ-0.15.0-03-01")
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        registry = get_registry()
        self.assertEqual(len(registry), 2)
        target_ids = {r.target.identifier for r in registry}
        self.assertEqual(target_ids, {"REQ-0.15.0-03-01", "REQ-0.15.0-03-02"})

    def test_linkage_source_includes_file_info(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        record = get_registry()[0]
        self.assertIsNotNone(record.source.location)
        self.assertIsNotNone(record.source.line)


class TestCoversMetadataOnly(unittest.TestCase):
    """@covers is metadata-only -- no behavior change."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    @covers("REQ-0.21.0-01-05")
    def test_return_value_preserved(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            return 42

        self.assertEqual(test_fn(), 42)

    def test_arguments_passed_through(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn(a, b, keyword=None):
            return (a, b, keyword)

        self.assertEqual(test_fn(1, 2, keyword="x"), (1, 2, "x"))

    def test_function_identity_preserved(self):
        def original_fn():
            pass

        decorated = covers("REQ-0.15.0-03-02")(original_fn)
        self.assertIs(decorated, original_fn)


class TestCoversWithTestCase(unittest.TestCase):
    """@covers works with unittest.TestCase methods."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    def test_unittest_method_registration(self):
        class SampleTest(unittest.TestCase):
            @covers("REQ-0.15.0-03-02")
            def test_sample(self):
                self.assertTrue(True)

        suite = unittest.TestLoader().loadTestsFromTestCase(SampleTest)
        result = unittest.TestResult()
        suite.run(result)

        self.assertEqual(result.testsRun, 1)
        self.assertEqual(len(result.failures), 0)
        self.assertEqual(len(result.errors), 0)

        registry = get_registry()
        self.assertEqual(len(registry), 1)
        self.assertIn("SampleTest.test_sample", registry[0].source.identifier)


class TestCoversWithStandaloneFunction(unittest.TestCase):
    """@covers works with standalone test functions."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    def test_standalone_function_registration(self):
        @covers("REQ-0.15.0-03-02")
        def standalone_test():
            return "result"

        self.assertEqual(standalone_test(), "result")

        registry = get_registry()
        self.assertEqual(len(registry), 1)
        self.assertIn("standalone_test", registry[0].source.identifier)


class TestGlobalRegistry(unittest.TestCase):
    """Global registry collects all registered linkages."""

    def setUp(self):
        reset_registry()
        set_known_reqs(_TEST_REQS)

    def tearDown(self):
        reset_registry()

    def test_registry_accumulates_across_decorators(self):
        @covers("REQ-0.15.0-03-01")
        def test_a():
            pass

        @covers("REQ-0.15.0-03-02")
        def test_b():
            pass

        @covers("REQ-0.20.0-01-01")
        def test_c():
            pass

        self.assertEqual(len(get_registry()), 3)

    def test_get_registry_returns_copy(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        registry = get_registry()
        registry.clear()
        self.assertEqual(len(get_registry()), 1)

    def test_reset_clears_registry(self):
        @covers("REQ-0.15.0-03-02")
        def test_fn():
            pass

        self.assertEqual(len(get_registry()), 1)
        reset_registry()
        set_known_reqs(_TEST_REQS)
        self.assertEqual(len(get_registry()), 0)


# ---------------------------------------------------------------------------
# OBPI-02: Coverage anchor scanner tests
# ---------------------------------------------------------------------------


def _make_req(semver: str, obpi_item: str, criterion: str, parent_obpi: str) -> DiscoveredReq:
    """Helper to create a DiscoveredReq for testing."""
    req_id = ReqId(semver=semver, obpi_item=obpi_item, criterion_index=criterion)
    entity = ReqEntity(
        id=req_id,
        description=f"Test criterion {criterion}",
        status=ReqStatus.UNCHECKED,
        parent_obpi=parent_obpi,
    )
    return DiscoveredReq(entity=entity, source_path="test_brief.md")


class TestScanTestTree(unittest.TestCase):
    """AST-based scanner discovers @covers annotations across test files."""

    @covers("REQ-0.21.0-02-01")
    def test_discovers_annotations_across_files(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_a.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    def test_alpha():
                        pass

                    @covers("REQ-0.15.0-03-02")
                    def test_beta():
                        pass
                """),
                encoding="utf-8",
            )
            (root / "test_b.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.20.0-01-01")
                    def test_gamma():
                        pass
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 3)
        target_ids = {r.target.identifier for r in records}
        self.assertEqual(target_ids, {"REQ-0.15.0-03-01", "REQ-0.15.0-03-02", "REQ-0.20.0-01-01"})

    def test_captures_line_numbers(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_lines.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    def test_first():
                        pass

                    @covers("REQ-0.15.0-03-02")
                    def test_second():
                        pass
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 2)
        self.assertEqual(records[0].evidence_line, 3)
        self.assertEqual(records[0].source.line, 4)
        self.assertEqual(records[1].evidence_line, 7)
        self.assertEqual(records[1].source.line, 8)

    def test_discovers_class_method_annotations(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_cls.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers
                    import unittest

                    class TestFoo(unittest.TestCase):
                        @covers("REQ-0.15.0-03-01")
                        def test_method(self):
                            pass
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 1)
        self.assertIn("TestFoo.test_method", records[0].source.identifier)

    def test_multiple_covers_on_same_function(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_multi.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    @covers("REQ-0.15.0-03-02")
                    def test_both():
                        pass
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 2)
        target_ids = {r.target.identifier for r in records}
        self.assertEqual(target_ids, {"REQ-0.15.0-03-01", "REQ-0.15.0-03-02"})

    def test_deterministic_output(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_z.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.20.0-01-01")
                    def test_z():
                        pass
                """),
                encoding="utf-8",
            )
            (root / "test_a.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    def test_a():
                        pass
                """),
                encoding="utf-8",
            )

            result1 = scan_test_tree(root)
            result2 = scan_test_tree(root)

        self.assertEqual(len(result1), len(result2))
        for r1, r2 in zip(result1, result2, strict=True):
            self.assertEqual(r1.target.identifier, r2.target.identifier)
            self.assertEqual(r1.source.identifier, r2.source.identifier)

    def test_skips_malformed_req(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_bad.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("INVALID-FORMAT")
                    def test_bad():
                        pass

                    @covers("REQ-0.15.0-03-01")
                    def test_good():
                        pass
                """),
                encoding="utf-8",
            )

            with self.assertLogs("gzkit.traceability", level="WARNING") as cm:
                records = scan_test_tree(root)

        self.assertEqual(len(records), 1)
        self.assertEqual(records[0].target.identifier, "REQ-0.15.0-03-01")
        self.assertTrue(any("Malformed REQ" in msg for msg in cm.output))

    def test_empty_directory(self):
        with tempfile.TemporaryDirectory() as tmp:
            records = scan_test_tree(Path(tmp))

        self.assertEqual(records, [])

    def test_no_execution_of_test_files(self):
        """Scanner must not import/execute files — syntax errors in body are OK."""
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / "test_exec.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    def test_would_crash():
                        raise RuntimeError("This should never execute during scanning")
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 1)
        self.assertEqual(records[0].target.identifier, "REQ-0.15.0-03-01")

    def test_scans_subdirectories(self):
        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            sub = root / "sub"
            sub.mkdir()
            (sub / "test_nested.py").write_text(
                textwrap.dedent("""\
                    from gzkit.traceability import covers

                    @covers("REQ-0.15.0-03-01")
                    def test_nested():
                        pass
                """),
                encoding="utf-8",
            )

            records = scan_test_tree(root)

        self.assertEqual(len(records), 1)


class TestComputeCoverage(unittest.TestCase):
    """Coverage rollup computation at ADR, OBPI, and REQ levels."""

    def _make_known_reqs(self) -> list[DiscoveredReq]:
        """Create a set of 8 known REQs across 2 ADRs and 3 OBPIs."""
        return [
            _make_req("0.15.0", "03", "01", "OBPI-0.15.0-03"),
            _make_req("0.15.0", "03", "02", "OBPI-0.15.0-03"),
            _make_req("0.15.0", "03", "03", "OBPI-0.15.0-03"),
            _make_req("0.15.0", "04", "01", "OBPI-0.15.0-04"),
            _make_req("0.15.0", "04", "02", "OBPI-0.15.0-04"),
            _make_req("0.20.0", "01", "01", "OBPI-0.20.0-01"),
            _make_req("0.20.0", "01", "02", "OBPI-0.20.0-01"),
            _make_req("0.20.0", "01", "03", "OBPI-0.20.0-01"),
        ]

    def _make_linkage_records(self) -> list:
        """Create 5 linkage records covering some of the 8 REQs."""
        from gzkit.triangle import LinkageRecord, VertexRef

        records = []
        covered_reqs = [
            "REQ-0.15.0-03-01",
            "REQ-0.15.0-03-02",
            "REQ-0.15.0-04-01",
            "REQ-0.20.0-01-01",
            "REQ-0.20.0-01-02",
        ]
        for req_str in covered_reqs:
            records.append(
                LinkageRecord(
                    source=VertexRef(
                        vertex_type=VertexType.TEST,
                        identifier=f"test_for_{req_str}",
                    ),
                    target=VertexRef(
                        vertex_type=VertexType.SPEC,
                        identifier=req_str,
                    ),
                    edge_type=EdgeType.COVERS,
                )
            )
        return records

    @covers("REQ-0.21.0-02-02")
    def test_overall_coverage_percentage(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        self.assertEqual(report.summary.total_reqs, 8)
        self.assertEqual(report.summary.covered_reqs, 5)
        self.assertEqual(report.summary.uncovered_reqs, 3)
        self.assertEqual(report.summary.coverage_percent, 62.5)

    @covers("REQ-0.21.0-02-03")
    def test_per_adr_rollup(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        adr_map = {r.identifier: r for r in report.by_adr}
        self.assertIn("ADR-0.15.0", adr_map)
        self.assertIn("ADR-0.20.0", adr_map)

        adr_15 = adr_map["ADR-0.15.0"]
        self.assertEqual(adr_15.total_reqs, 5)
        self.assertEqual(adr_15.covered_reqs, 3)
        self.assertEqual(adr_15.coverage_percent, 60.0)

        adr_20 = adr_map["ADR-0.20.0"]
        self.assertEqual(adr_20.total_reqs, 3)
        self.assertEqual(adr_20.covered_reqs, 2)
        self.assertAlmostEqual(adr_20.coverage_percent, 66.7, places=1)

    def test_per_obpi_rollup(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        obpi_map = {r.identifier: r for r in report.by_obpi}
        self.assertIn("OBPI-0.15.0-03", obpi_map)
        self.assertIn("OBPI-0.15.0-04", obpi_map)
        self.assertIn("OBPI-0.20.0-01", obpi_map)

        obpi_03 = obpi_map["OBPI-0.15.0-03"]
        self.assertEqual(obpi_03.total_reqs, 3)
        self.assertEqual(obpi_03.covered_reqs, 2)

        obpi_04 = obpi_map["OBPI-0.15.0-04"]
        self.assertEqual(obpi_04.total_reqs, 2)
        self.assertEqual(obpi_04.covered_reqs, 1)

    def test_per_req_entries(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        self.assertEqual(len(report.entries), 8)
        entry_map = {e.req_id: e for e in report.entries}

        self.assertTrue(entry_map["REQ-0.15.0-03-01"].covered)
        self.assertFalse(entry_map["REQ-0.15.0-03-03"].covered)
        self.assertTrue(entry_map["REQ-0.20.0-01-01"].covered)
        self.assertFalse(entry_map["REQ-0.20.0-01-03"].covered)

    def test_covering_tests_listed(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        entry_map = {e.req_id: e for e in report.entries}
        self.assertEqual(len(entry_map["REQ-0.15.0-03-01"].covering_tests), 1)
        self.assertEqual(len(entry_map["REQ-0.15.0-03-03"].covering_tests), 0)

    def test_empty_inputs(self):
        report = compute_coverage([], [])

        self.assertEqual(report.summary.total_reqs, 0)
        self.assertEqual(report.summary.coverage_percent, 0.0)
        self.assertEqual(report.entries, [])

    def test_no_linkages(self):
        known = self._make_known_reqs()
        report = compute_coverage(known, [])

        self.assertEqual(report.summary.total_reqs, 8)
        self.assertEqual(report.summary.covered_reqs, 0)
        self.assertEqual(report.summary.coverage_percent, 0.0)

    def test_deterministic_computation(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        r1 = compute_coverage(known, linkages)
        r2 = compute_coverage(known, linkages)

        self.assertEqual(r1.summary, r2.summary)
        self.assertEqual(len(r1.entries), len(r2.entries))
        for e1, e2 in zip(r1.entries, r2.entries, strict=True):
            self.assertEqual(e1.req_id, e2.req_id)
            self.assertEqual(e1.covered, e2.covered)

    def test_report_is_pydantic_model(self):
        known = self._make_known_reqs()
        linkages = self._make_linkage_records()
        report = compute_coverage(known, linkages)

        self.assertIsInstance(report, CoverageReport)
        json_str = report.model_dump_json()
        self.assertIn("by_adr", json_str)
        self.assertIn("by_obpi", json_str)


# ---------------------------------------------------------------------------
# OBPI-03: gz covers CLI tests
# ---------------------------------------------------------------------------


class TestCoversCLIHelp(unittest.TestCase):
    """gz covers --help works and shows required elements."""

    @covers("REQ-0.21.0-03-04")
    def test_help_exits_zero(self):
        import io
        from contextlib import redirect_stderr, redirect_stdout

        from gzkit.cli import main

        out = io.StringIO()
        with redirect_stdout(out), redirect_stderr(out):
            try:
                code = main(["covers", "--help"])
            except SystemExit as exc:
                code = exc.code
        self.assertEqual(code, 0)
        text = out.getvalue()
        self.assertIn("--json", text)
        self.assertIn("--plain", text)

    def test_help_shows_description(self):
        import contextlib
        import io
        from contextlib import redirect_stderr, redirect_stdout

        from gzkit.cli import main

        out = io.StringIO()
        with redirect_stdout(out), redirect_stderr(out), contextlib.suppress(SystemExit):
            main(["covers", "--help"])
        text = out.getvalue()
        self.assertIn("coverage", text.lower())

    def test_help_shows_examples(self):
        import contextlib
        import io
        from contextlib import redirect_stderr, redirect_stdout

        from gzkit.cli import main

        out = io.StringIO()
        with redirect_stdout(out), redirect_stderr(out), contextlib.suppress(SystemExit):
            main(["covers", "--help"])
        text = out.getvalue()
        self.assertIn("gz covers", text)


class TestCoversCLIOutput(unittest.TestCase):
    """gz covers produces correct output in all three modes."""

    def setUp(self):
        self._tmpdir = tempfile.TemporaryDirectory()
        self.tmp = Path(self._tmpdir.name)
        self.adr_dir = self.tmp / "design" / "adr" / "pre-release" / "ADR-0.15.0"
        self.adr_dir.mkdir(parents=True)
        obpis = self.adr_dir / "obpis"
        obpis.mkdir()
        (obpis / "OBPI-0.15.0-03-demo.md").write_text(
            "---\n"
            "id: OBPI-0.15.0-03-demo\n"
            "parent: ADR-0.15.0\n"
            "item: 3\n"
            "lane: Lite\n"
            "status: Accepted\n"
            "---\n\n"
            "# OBPI-0.15.0-03: Demo\n\n"
            "## Acceptance Criteria\n\n"
            "- [ ] REQ-0.15.0-03-01: First criterion.\n"
            "- [ ] REQ-0.15.0-03-02: Second criterion.\n",
            encoding="utf-8",
        )

        self.test_dir = self.tmp / "tests"
        self.test_dir.mkdir()
        (self.test_dir / "test_demo.py").write_text(
            textwrap.dedent("""\
                from gzkit.traceability import covers

                @covers("REQ-0.15.0-03-01")
                def test_one():
                    pass
            """),
            encoding="utf-8",
        )

    def tearDown(self):
        self._tmpdir.cleanup()

    def _run_covers(self, extra_args: list[str] | None = None) -> tuple[int, str]:
        import io
        from contextlib import redirect_stderr, redirect_stdout

        from gzkit.cli import main

        args = [
            "covers",
            "--adr-dir",
            str(self.adr_dir.parent),
            "--test-dir",
            str(self.test_dir),
        ]
        if extra_args:
            args.extend(extra_args)
        out = io.StringIO()
        with redirect_stdout(out), redirect_stderr(out):
            try:
                code = main(args)
            except SystemExit as exc:
                code = exc.code
        return 0 if code is None else int(code), out.getvalue()

    def test_exit_code_always_zero(self):
        code, _ = self._run_covers()
        self.assertEqual(code, 0)

    @covers("REQ-0.21.0-03-03")
    def test_json_output_valid(self):
        import json

        code, output = self._run_covers(["--json"])
        self.assertEqual(code, 0)
        data = json.loads(output)
        self.assertIn("by_adr", data)
        self.assertIn("by_obpi", data)
        self.assertIn("entries", data)
        self.assertIn("summary", data)

    def test_json_shows_coverage_data(self):
        import json

        _, output = self._run_covers(["--json"])
        data = json.loads(output)
        self.assertEqual(data["summary"]["total_reqs"], 2)
        self.assertEqual(data["summary"]["covered_reqs"], 1)

    def test_plain_one_record_per_line(self):
        _, output = self._run_covers(["--plain"])
        lines = output.strip().splitlines()
        self.assertEqual(len(lines), 2)
        self.assertIn("covered", lines[0])
        self.assertIn("uncovered", lines[1])

    @covers("REQ-0.21.0-03-01")
    def test_human_shows_summary(self):
        code, output = self._run_covers()
        self.assertEqual(code, 0)
        self.assertIn("Summary", output)

    @covers("REQ-0.21.0-03-02")
    def test_filter_by_adr(self):
        import json

        _, output = self._run_covers(["ADR-0.15.0", "--json"])
        data = json.loads(output)
        self.assertEqual(data["summary"]["total_reqs"], 2)

    def test_filter_by_obpi(self):
        import json

        _, output = self._run_covers(["OBPI-0.15.0-03", "--json"])
        data = json.loads(output)
        self.assertEqual(data["summary"]["total_reqs"], 2)

    def test_filter_nonexistent_adr_returns_empty(self):
        import json

        _, output = self._run_covers(["ADR-9.9.9", "--json"])
        data = json.loads(output)
        self.assertEqual(data["summary"]["total_reqs"], 0)


# ---------------------------------------------------------------------------
# OBPI-04: ADR audit integration tests
# ---------------------------------------------------------------------------


class TestExtractAdrSemver(unittest.TestCase):
    """_extract_adr_semver extracts X.Y.Z from ADR identifiers."""

    def test_simple_adr_id(self):
        from gzkit.commands.adr_audit import _extract_adr_semver

        self.assertEqual(_extract_adr_semver("ADR-0.20.0"), "0.20.0")

    def test_adr_with_suffix(self):
        from gzkit.commands.adr_audit import _extract_adr_semver

        self.assertEqual(_extract_adr_semver("ADR-0.21.0-tests-for-spec"), "0.21.0")

    def test_invalid_format_returns_none(self):
        from gzkit.commands.adr_audit import _extract_adr_semver

        self.assertIsNone(_extract_adr_semver("INVALID"))

    def test_pool_adr_returns_none(self):
        from gzkit.commands.adr_audit import _extract_adr_semver

        self.assertIsNone(_extract_adr_semver("ADR-pool.my-feature"))


class TestComputeAdrCoverage(unittest.TestCase):
    """_compute_adr_coverage returns scoped coverage data for an ADR."""

    def setUp(self):
        self._tmpdir = tempfile.TemporaryDirectory()
        self.root = Path(self._tmpdir.name)
        self.adr_dir = self.root / "docs" / "design" / "adr"

        # Create ADR/OBPI brief structure with 2 REQs
        adr_pkg = self.adr_dir / "ADR-0.15.0"
        obpis = adr_pkg / "obpis"
        obpis.mkdir(parents=True)
        (obpis / "OBPI-0.15.0-03-demo.md").write_text(
            "---\n"
            "id: OBPI-0.15.0-03-demo\n"
            "parent: ADR-0.15.0\n"
            "item: 3\n"
            "lane: Lite\n"
            "status: Accepted\n"
            "---\n\n"
            "# OBPI-0.15.0-03: Demo\n\n"
            "## Acceptance Criteria\n\n"
            "- [ ] REQ-0.15.0-03-01: First criterion.\n"
            "- [ ] REQ-0.15.0-03-02: Second criterion.\n",
            encoding="utf-8",
        )

        # Create test file covering only REQ-01
        tests_dir = self.root / "tests"
        tests_dir.mkdir()
        (tests_dir / "test_sample.py").write_text(
            textwrap.dedent("""\
                from gzkit.traceability import covers

                @covers("REQ-0.15.0-03-01")
                def test_one():
                    pass
            """),
            encoding="utf-8",
        )

    def tearDown(self):
        self._tmpdir.cleanup()

    @covers("REQ-0.21.0-04-01")
    def test_returns_coverage_for_adr(self):
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-0.15.0", self.adr_dir)
        self.assertEqual(result["total_reqs"], 2)
        self.assertEqual(result["covered_reqs"], 1)
        self.assertEqual(result["uncovered_reqs"], 1)
        self.assertEqual(result["coverage_percent"], 50.0)

    @covers("REQ-0.21.0-04-03")
    def test_includes_per_obpi_data(self):
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-0.15.0", self.adr_dir)
        self.assertEqual(len(result["by_obpi"]), 1)
        obpi = result["by_obpi"][0]
        self.assertEqual(obpi["obpi"], "OBPI-0.15.0-03")
        self.assertEqual(obpi["total_reqs"], 2)
        self.assertEqual(obpi["covered_reqs"], 1)

    @covers("REQ-0.21.0-04-02")
    def test_uncovered_reqs_listed(self):
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-0.15.0", self.adr_dir)
        self.assertEqual(len(result["uncovered"]), 1)
        self.assertEqual(result["uncovered"][0]["req_id"], "REQ-0.15.0-03-02")
        self.assertEqual(result["uncovered"][0]["severity"], "advisory")

    def test_nonexistent_adr_returns_empty(self):
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-9.9.9", self.adr_dir)
        self.assertEqual(result["total_reqs"], 0)
        self.assertEqual(result["by_obpi"], [])

    def test_invalid_adr_returns_empty(self):
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "INVALID", self.adr_dir)
        self.assertEqual(result["total_reqs"], 0)

    def test_coverage_does_not_block_audit(self):
        """Coverage is advisory — uncovered REQs never make passed=False."""
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-0.15.0", self.adr_dir)
        # Coverage data has uncovered REQs but no "passed" key —
        # pass/fail is determined by findings, not coverage
        self.assertNotIn("passed", result)
        self.assertEqual(result["uncovered_reqs"], 1)

    def test_all_covered_returns_empty_uncovered(self):
        """When all REQs are covered, uncovered list is empty."""
        (self.root / "tests" / "test_sample2.py").write_text(
            textwrap.dedent("""\
                from gzkit.traceability import covers

                @covers("REQ-0.15.0-03-02")
                def test_two():
                    pass
            """),
            encoding="utf-8",
        )
        from gzkit.commands.adr_audit import _compute_adr_coverage

        result = _compute_adr_coverage(self.root, "ADR-0.15.0", self.adr_dir)
        self.assertEqual(result["covered_reqs"], 2)
        self.assertEqual(result["uncovered"], [])
