"""Tests for ci_gate.main — build_ci_gate_plan and compute_execution_plan."""

from __future__ import annotations

from datetime import date
from pathlib import Path

import pytest

from scripts.helpers.ci_gate.gate_policy import SourceExemption, TestExemption, default_test_discovery
from scripts.helpers.ci_gate.main import (
    build_ci_gate_plan,
    build_coverage_mapping_errors,
    build_hard_blocking_plan,
    compute_execution_plan,
)
from scripts.helpers.ci_gate.models import Baseline, ChangeSet


def _sample_exemption(file: str, symbol: str) -> SourceExemption:
    return SourceExemption(
        file=file,
        symbol=symbol,
        reason="test",
        applicant="test",
        approver="fangkai",
        deadline=date(2099, 12, 31),
    )


@pytest.fixture(scope="module")
def baseline() -> Baseline:
    test_map = {
        "cli/main.py": {
            "run": ["tests/regression/cli/test_run.py::test_run"],
        },
        "tensor_cast/ops.py": {
            "add": [
                "tests/regression/tensor_cast/test_ops.py::test_add",
                "tests/regression/cli/test_cross.py::test_cross",
            ],
        },
    }
    return Baseline(
        test_map=test_map,
        exemptions=(),
        test_exemptions=(),
        discovery=default_test_discovery(),
        roots=(
            "cli/",
            "tensor_cast/",
            "serving_cast/",
            "web_ui/",
            "scripts/",
            "tools/",
        ),
    )


@pytest.fixture(autouse=True)
def _stub_collect_test_node_ids(monkeypatch: pytest.MonkeyPatch) -> None:
    def _fake_collect(targets: tuple[str, ...] | list[str], *, marker: str) -> tuple[str, ...]:
        return tuple(f"{path}::test_case" for path in targets)

    monkeypatch.setattr(
        "scripts.helpers.ci_gate.rules.collect_test_node_ids",
        _fake_collect,
    )


def test_plan_config_change_triggers_full_suite(baseline: Baseline) -> None:
    cs = ChangeSet.build(config=("pyproject.toml",))
    plan = build_ci_gate_plan(Path("/tmp"), cs, baseline)
    assert plan.full_suite is True


def test_plan_new_test_selects_collected_nodes(baseline: Baseline) -> None:
    cs = ChangeSet.build(new_test=("tests/smoke/test_new.py",))
    plan = build_ci_gate_plan(Path("/tmp"), cs, baseline)
    assert plan.changed_test_nodes == frozenset({"tests/smoke/test_new.py::test_case"})


def test_plan_all_exempt_changed_test_file_not_scheduled(
    baseline: Baseline,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    exempt_baseline = Baseline(
        test_map=baseline.test_map,
        exemptions=baseline.exemptions,
        test_exemptions=(
            TestExemption(
                test_id="tests/smoke/test_new.py::test_case",
                reason="x",
                applicant="a",
                approver="fangkai",
                deadline=date(2099, 12, 31),
            ),
        ),
        discovery=baseline.discovery,
        roots=baseline.roots,
    )
    cs = ChangeSet.build(new_test=("tests/smoke/test_new.py",))
    plan = build_ci_gate_plan(Path("/tmp"), cs, exempt_baseline)
    assert plan.changed_test_nodes == frozenset()


def test_plan_deleted_test_sole_coverage_blocking(baseline: Baseline) -> None:
    cs = ChangeSet.build(del_test=("tests/regression/cli/test_run.py::test_run",))
    errors = build_hard_blocking_plan(cs, baseline.test_map, baseline.roots)
    assert len(errors) == 1
    assert errors[0].category == "deleted_test"


def test_plan_modified_source_selects_regression_tests(baseline: Baseline, tmp_path: Path) -> None:
    src = tmp_path / "cli" / "main.py"
    src.parent.mkdir(parents=True)
    src.write_text("def run():\n    x = 1\n", encoding="utf-8")
    cs = ChangeSet.build(modified_source={"cli/main.py": frozenset({2})})
    plan = build_ci_gate_plan(tmp_path, cs, baseline)
    assert "tests/regression/cli/test_run.py::test_run" in plan.regression_tests


def test_plan_deleted_source_selects_guard_tests(baseline: Baseline) -> None:
    cs = ChangeSet.build(del_source=("cli/main.py",))
    plan = build_ci_gate_plan(Path("/tmp"), cs, baseline)
    assert "tests/regression/cli/test_run.py::test_run" in plan.deleted_source_tests


def test_plan_blocking_errors_combined(baseline: Baseline, tmp_path: Path) -> None:
    cs = ChangeSet.build(
        del_source=("tensor_cast/unknown.py",),
        del_test=("tests/regression/cli/test_run.py::test_run",),
    )
    errors = build_hard_blocking_plan(cs, baseline.test_map, baseline.roots)
    assert len(errors) == 2


def test_plan_new_source_with_exemption_no_error(baseline: Baseline, tmp_path: Path) -> None:
    src = tmp_path / "tensor_cast" / "new_mod.py"
    src.parent.mkdir(parents=True)
    src.write_text("def fn():\n    pass\n", encoding="utf-8")
    cs = ChangeSet.build(new_source=("tensor_cast/new_mod.py",))
    exempt_baseline = Baseline(
        test_map=baseline.test_map,
        exemptions=(_sample_exemption("tensor_cast/new_mod.py", "fn"),),
        test_exemptions=(),
        discovery=baseline.discovery,
        roots=baseline.roots,
    )
    errors = build_coverage_mapping_errors(
        tmp_path,
        cs,
        exempt_baseline.test_map,
        exempt_baseline.exemptions,
        exempt_baseline.roots,
        coverage_path=tmp_path / ".coverage",
    )
    assert len(errors) == 0


def test_compute_execution_plan_full_suite() -> None:
    from scripts.helpers.ci_gate.models import CiGatePlan

    plan = CiGatePlan(
        blocking_errors=(),
        deleted_source_tests=frozenset(),
        changed_test_nodes=frozenset(),
        regression_tests=frozenset(),
        full_suite=True,
    )
    execution = compute_execution_plan(plan, ())
    assert execution.full_suite is True
    assert execution.waves[0].targets == ("tests",)
    assert execution.waves[0].marker == "not npu"


def test_plan_config_change_skips_changed_test_collection(baseline: Baseline) -> None:
    cs = ChangeSet.build(
        config=("pyproject.toml",),
        new_test=("tests/smoke/test_new.py",),
    )
    plan = build_ci_gate_plan(Path("/tmp"), cs, baseline)
    assert plan.full_suite is True
    assert plan.changed_test_nodes == frozenset()


def test_compute_execution_plan_dedupes_changed_and_regression() -> None:
    from scripts.helpers.ci_gate.models import CiGatePlan

    shared = "tests/regression/cli/test_shared.py::test_x"
    plan = CiGatePlan(
        blocking_errors=(),
        deleted_source_tests=frozenset(),
        changed_test_nodes=frozenset({shared}),
        regression_tests=frozenset({shared, "tests/regression/cli/test_other.py::test_y"}),
        full_suite=False,
    )
    execution = compute_execution_plan(plan, ())
    all_nodes = [node for wave in execution.waves for node in wave.targets]
    assert all_nodes.count(shared) == 1
    assert len(execution.waves) == 2
    assert execution.waves[0].marker == "not npu"
    assert execution.waves[1].marker == "not npu and not nightly and not network"