"""Tests for the separate line/branch coverage policy gate."""

from __future__ import annotations

from scripts import check_coverage_thresholds as MODULE


def _report(line: float = 55.0, branch: float = 42.0) -> dict[str, object]:
    return {
        "totals": {
            "percent_statements_covered": line,
            "percent_branches_covered": branch,
        },
        "files": {
            "obliteratus/example.py": {
                "summary": {"percent_statements_covered": 70.0},
                "executed_lines": [1, 2, 4],
                "missing_lines": [3],
            },
        },
    }


def test_validate_coverage_accepts_exact_floors():
    assert MODULE.validate_coverage(
        _report(), min_line=55.0, min_branch=42.0,
    ) == []


def test_validate_coverage_reports_each_regression():
    failures = MODULE.validate_coverage(
        _report(line=54.9, branch=41.9), min_line=55.0, min_branch=42.0,
    )

    assert failures == [
        "line coverage 54.90% is below the 55.00% floor",
        "branch coverage 41.90% is below the 42.00% floor",
    ]


def test_validate_coverage_rejects_malformed_totals():
    assert MODULE.validate_coverage(
        {}, min_line=55.0, min_branch=42.0,
    ) == ["coverage report is missing the totals object"]


def test_validate_coverage_rejects_non_numeric_metrics():
    report = _report()
    report["totals"]["percent_statements_covered"] = True
    report["totals"]["percent_branches_covered"] = float("nan")

    assert MODULE.validate_coverage(
        report, min_line=55.0, min_branch=42.0,
    ) == [
        "coverage report is missing numeric percent_statements_covered",
        "coverage report is missing numeric percent_branches_covered",
    ]


def test_validate_coverage_enforces_critical_file_floors():
    assert MODULE.validate_coverage(
        _report(),
        min_line=55.0,
        min_branch=42.0,
        file_floors={"obliteratus/example.py": 70.0},
    ) == []

    report = _report()
    report["files"]["obliteratus/example.py"]["summary"]["percent_statements_covered"] = 69.9
    assert MODULE.validate_coverage(
        report,
        min_line=55.0,
        min_branch=42.0,
        file_floors={"obliteratus/example.py": 70.0, "missing.py": 70.0},
    ) == [
        "critical file obliteratus/example.py coverage 69.90% is below the 70.00% floor",
        "coverage report is missing critical file missing.py",
    ]


def test_critical_file_validation_continues_and_rejects_malformed_values():
    report = _report()
    report["files"]["bool.py"] = {
        "summary": {"percent_statements_covered": True},
    }
    report["files"]["text.py"] = {
        "summary": {"percent_statements_covered": "70"},
    }
    assert MODULE.validate_coverage(
        report,
        min_line=55.0,
        min_branch=42.0,
        file_floors={
            "missing.py": 70.0,
            "bool.py": 70.0,
            "text.py": 70.0,
        },
    ) == [
        "coverage report is missing critical file missing.py",
        "coverage report is missing numeric line coverage for bool.py",
        "coverage report is missing numeric line coverage for text.py",
    ]


def test_parse_changed_lines_and_measurement_ignore_non_executable_lines():
    diff = """diff --git a/obliteratus/example.py b/obliteratus/example.py
+++ b/obliteratus/example.py
@@ -1,2 +1,4 @@
diff --git a/tests/test_example.py b/tests/test_example.py
+++ b/tests/test_example.py
@@ -0,0 +1,2 @@
"""
    changed = MODULE.parse_changed_lines(diff)
    assert changed == {
        "obliteratus/example.py": {1, 2, 3, 4},
        "tests/test_example.py": {1, 2},
    }
    assert MODULE.changed_line_coverage(_report(), changed) == (3, 4, 75.0)
    assert MODULE.validate_changed_coverage(
        _report(), changed, minimum=90.0,
    ) == ["changed-line coverage 75.00% (3/4) is below the 90.00% floor"]


def test_changed_line_gate_passes_when_diff_has_no_measured_source():
    assert MODULE.changed_line_coverage(_report(), {"tests/test_example.py": {1}}) == (
        0,
        0,
        100.0,
    )


def test_changed_line_parser_ignores_hunks_before_paths_and_defaults_count_to_one():
    diff = """@@ -1 +99 @@
+++ b/obliteratus/example.py
@@ -2 +3 @@
"""
    assert MODULE.parse_changed_lines(diff) == {"obliteratus/example.py": {3}}


def test_changed_line_measurement_handles_missing_fields_and_multiple_files():
    report = {
        "files": {
            "obliteratus/no-fields.py": {"summary": {}},
            "obliteratus/first.py": {
                "executed_lines": [1],
                "missing_lines": [2],
            },
            "obliteratus/second.py": {
                "executed_lines": [4],
                "missing_lines": [5],
            },
        },
    }
    changed = {
        "missing-entry.py": {1},
        "obliteratus/no-fields.py": {1},
        "obliteratus/first.py": {1, 2, 99},
        "obliteratus/second.py": {4, 5, 100},
    }
    assert MODULE.changed_line_coverage(report, changed) == (2, 4, 50.0)


def test_changed_line_gate_accepts_exact_floor():
    report = _report()
    report["files"]["obliteratus/example.py"]["executed_lines"] = list(range(1, 20))
    report["files"]["obliteratus/example.py"]["missing_lines"] = [20]
    changed = {"obliteratus/example.py": set(range(1, 21))}
    assert MODULE.validate_changed_coverage(
        report, changed, minimum=95.0,
    ) == []
    assert MODULE.validate_changed_coverage(
        report, changed, minimum=95.01,
    ) == ["changed-line coverage 95.00% (19/20) is below the 95.01% floor"]


def _module_report(
    path: str = "obliteratus/example.py",
    *,
    covered_lines: int = 8,
    statements: int = 10,
    covered_branches: int = 3,
    branches: int = 4,
) -> dict[str, object]:
    return {
        "files": {
            path: {
                "summary": {
                    "covered_lines": covered_lines,
                    "num_statements": statements,
                    "covered_branches": covered_branches,
                    "num_branches": branches,
                },
            },
        },
    }


def test_parse_changed_modules_handles_nul_renames_and_filters_nonproduction():
    raw = (
        b"M\0obliteratus/current.py\0"
        b"A\0tests/test_current.py\0"
        b"D\0obliteratus/deleted.py\0"
        b"R100\0obliteratus/old.py\0obliteratus/new.py\0"
        b"R100\0obliteratus/removed.py\0docs/removed.py\0"
        b"C100\0obliteratus/source.py\0obliteratus/copied.py\0"
    )

    assert MODULE.parse_changed_modules(raw) == [
        MODULE.ChangedModule("M", "obliteratus/current.py", "obliteratus/current.py"),
        MODULE.ChangedModule("D", "obliteratus/deleted.py", "obliteratus/deleted.py"),
        MODULE.ChangedModule("R100", "obliteratus/old.py", "obliteratus/new.py"),
        MODULE.ChangedModule("D", "obliteratus/removed.py", "obliteratus/removed.py"),
        MODULE.ChangedModule("C100", None, "obliteratus/copied.py"),
    ]


def test_parse_changed_modules_rejects_truncated_and_unknown_statuses():
    import pytest

    with pytest.raises(ValueError, match="truncated"):
        MODULE.parse_changed_modules(b"R100\0obliteratus/old.py\0")
    with pytest.raises(ValueError, match="unsupported"):
        MODULE.parse_changed_modules(b"U\0obliteratus/example.py\0")


def test_touched_module_gate_reports_line_and_branch_regressions_separately():
    base = _module_report(covered_lines=9, covered_branches=4)
    head = _module_report(covered_lines=8, covered_branches=3)
    changes = [MODULE.ChangedModule("M", "obliteratus/example.py", "obliteratus/example.py")]

    assert MODULE.validate_touched_module_regression(head, base, changes) == [
        "touched module obliteratus/example.py line coverage regressed from 90.00% to 80.00%",
        "touched module obliteratus/example.py branch coverage regressed from 100.00% to 75.00%",
    ]


def test_touched_module_gate_applies_tolerances_to_existing_module():
    base = _module_report(covered_lines=801, statements=1000, covered_branches=751, branches=1000)
    head = _module_report(covered_lines=800, statements=1000, covered_branches=750, branches=1000)
    changes = [MODULE.ChangedModule("M", "obliteratus/example.py", "obliteratus/example.py")]

    assert MODULE.validate_touched_module_regression(
        head,
        base,
        changes,
        line_tolerance=0.1,
        branch_tolerance=0.1,
    ) == []
    assert len(MODULE.validate_touched_module_regression(
        head,
        base,
        changes,
        line_tolerance=0.09,
        branch_tolerance=0.09,
    )) == 2


def test_touched_module_gate_enforces_new_module_floors():
    head = _module_report(covered_lines=7, statements=10, covered_branches=2, branches=4)
    changes = [MODULE.ChangedModule("A", None, "obliteratus/example.py")]

    assert MODULE.validate_touched_module_regression(head, {}, changes) == [
        "new module obliteratus/example.py line coverage 70.00% is below the 80.00% floor",
        "new module obliteratus/example.py branch coverage 50.00% is below the 75.00% floor",
    ]


def test_touched_module_gate_requires_floor_when_existing_module_adds_branches():
    base = _module_report(covered_branches=0, branches=0)
    head = _module_report(covered_branches=2, branches=4)
    changes = [MODULE.ChangedModule("M", "obliteratus/example.py", "obliteratus/example.py")]

    assert MODULE.validate_touched_module_regression(head, base, changes) == [
        "touched module obliteratus/example.py added branches at 50.00% coverage, "
        "below the 75.00% floor",
    ]


def test_touched_module_gate_accepts_branch_removal_and_rename():
    base = _module_report("obliteratus/old.py", covered_branches=3, branches=4)
    head = _module_report("obliteratus/new.py", covered_branches=0, branches=0)
    changes = [MODULE.ChangedModule("R100", "obliteratus/old.py", "obliteratus/new.py")]

    assert MODULE.validate_touched_module_regression(head, base, changes) == []


def test_touched_module_gate_rejects_missing_and_malformed_measurements():
    changes = [MODULE.ChangedModule("M", "obliteratus/example.py", "obliteratus/example.py")]
    assert MODULE.validate_touched_module_regression({}, _module_report(), changes) == [
        "coverage report is missing touched module obliteratus/example.py",
    ]

    malformed = _module_report()
    malformed["files"]["obliteratus/example.py"]["summary"]["covered_lines"] = True
    assert MODULE.validate_touched_module_regression(malformed, _module_report(), changes) == [
        "coverage report has invalid covered_lines for touched module obliteratus/example.py",
    ]


def test_touched_module_gate_rejects_missing_base_measurement():
    changes = [MODULE.ChangedModule("M", "obliteratus/example.py", "obliteratus/example.py")]
    assert MODULE.validate_touched_module_regression(_module_report(), {}, changes) == [
        "base coverage report is missing touched module obliteratus/example.py",
    ]


def test_touched_module_gate_requires_reviewed_exception_for_deletion():
    changes = [MODULE.ChangedModule("D", "obliteratus/example.py", "obliteratus/example.py")]
    assert MODULE.validate_touched_module_regression({}, _module_report(), changes) == [
        "deleted production module obliteratus/example.py requires an explicit reviewed "
        "coverage-policy exception",
    ]
