"""Extra regression tests for scripts.helpers.ci_gate.main."""

from __future__ import annotations

import logging
from datetime import date
from pathlib import Path

import pytest

from scripts.helpers._config import Config, ConfigError
from scripts.helpers.ci_gate.diff import GitDiffResult
from scripts.helpers.ci_gate.gate_policy import TestExemption, default_test_discovery
from scripts.helpers.ci_gate.main import (
    _log_blocking_errors,
    _log_execution_plan,
    _remap_renamed_sources,
    main,
)
from scripts.helpers.ci_gate.models import Baseline, ChangeSet, CiGatePlan, ExecutionPlan, GateError, TestRunWave


def _empty_diff() -> GitDiffResult:
    return GitDiffResult(line_map={}, entries=())


@pytest.fixture
def gate_cfg() -> Config:
    return Config(
        test_map_path="/tmp/test_map.json",
        base_branch="develop",
        line_threshold=60.0,
        branch_threshold=40.0,
        benchmark_parallel=False,
        feishu_webhook_url="",
        msmodeling_cache=".msmodeling_cache",
        weights_prune=True,
    )


@pytest.fixture
def baseline() -> Baseline:
    return Baseline(
        test_map={
            "cli/old.py": {
                "run": ["tests/regression/cli/test_old.py::test_old"],
            },
        },
        exemptions=(),
        discovery=default_test_discovery(),
        test_exemptions=(),
        roots=(
            "cli/",
            "tensor_cast/",
            "serving_cast/",
            "web_ui/",
            "scripts/",
            "tools/",
        ),
    )


def test_remap_renamed_sources_moves_map_entries() -> None:
    test_map = {"cli/old.py": {"run": ["tests/regression/cli/test_old.py::test_old"]}}

    remapped = _remap_renamed_sources(test_map, (("cli/old.py", "cli/new.py", 100),))

    assert "cli/old.py" not in remapped
    assert remapped == {"cli/new.py": {"run": ["tests/regression/cli/test_old.py::test_old"]}}


def test_log_blocking_errors_emits_category_summary(caplog: pytest.LogCaptureFixture) -> None:
    errors = (
        GateError(category="deleted_source", path="cli/old.py"),
        GateError(category="deleted_source", path="cli/other.py"),
        GateError(category="modified_source", path="cli/main.py"),
    )

    with caplog.at_level(logging.ERROR, logger="ci_gate"):
        _log_blocking_errors(logging.getLogger("ci_gate"), errors)

    assert "deleted_source=2" in caplog.text
    assert "modified_source=1" in caplog.text
    assert "Phase" not in caplog.text


def test_log_execution_plan_logs_reason_counts(caplog: pytest.LogCaptureFixture) -> None:
    execution = ExecutionPlan(
        full_suite=False,
        waves=(
            TestRunWave(
                targets=("tests/regression/cli/test_a.py::test_a",),
                marker="not npu",
            ),
        ),
        reasons={
            "tests/regression/cli/test_a.py::test_a": "new or changed test file",
            "tests/regression/cli/test_b.py::test_b": "changed product file mapped regression",
        },
    )

    with caplog.at_level(logging.INFO, logger="ci_gate"):
        _log_execution_plan(logging.getLogger("ci_gate"), execution)

    assert "new or changed test file" in caplog.text
    assert "changed product file mapped regression" in caplog.text
    assert "Phase" not in caplog.text


def test_main_returns_one_on_resolve_base_ref_error(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
) -> None:
    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: (_ for _ in ()).throw(ConfigError("base ref"))
    )

    assert main() == 1


def test_main_returns_one_on_load_baseline_error(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
) -> None:
    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.load_baseline", lambda *_args: (_ for _ in ()).throw(ConfigError("baseline"))
    )

    assert main() == 1


def test_main_remaps_before_plan_without_phase0_merge(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
    baseline: Baseline,
) -> None:
    captured: dict[str, Baseline] = {}

    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.load_baseline", lambda *_args: baseline)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.fetch_diff", lambda *_args: _empty_diff())
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.classify_changes",
        lambda *_args: ChangeSet.build(
            new_test=("tests/regression/cli/test_new.py",),
            renames=(("cli/old.py", "cli/new.py", 100),),
        ),
    )

    def _fake_build_plan(
        _repo_root: Path,
        _changes: ChangeSet,
        new_baseline: Baseline,
        **_kwargs: object,
    ) -> CiGatePlan:
        captured["baseline"] = new_baseline
        return CiGatePlan(
            blocking_errors=(),
            deleted_source_tests=frozenset(),
            changed_test_nodes=frozenset(),
            regression_tests=frozenset(),
            full_suite=False,
        )

    monkeypatch.setattr("scripts.helpers.ci_gate.main.build_ci_gate_plan", _fake_build_plan)

    assert main() == 0
    assert "cli/new.py" in captured["baseline"].test_map
    assert captured["baseline"].test_map["cli/new.py"]["run"] == ["tests/regression/cli/test_old.py::test_old"]


def test_main_runs_deleted_source_guards_in_union(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
    baseline: Baseline,
) -> None:
    pytest_calls: list[tuple[list[str], str]] = []

    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.load_baseline", lambda *_args: baseline)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.fetch_diff", lambda *_args: _empty_diff())
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.classify_changes",
        lambda *_args: ChangeSet.build(del_source=("cli/old.py",)),
    )
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.build_ci_gate_plan",
        lambda *_args, **_kwargs: CiGatePlan(
            blocking_errors=(),
            deleted_source_tests=frozenset({"tests/regression/cli/test_old.py::test_old"}),
            changed_test_nodes=frozenset(),
            regression_tests=frozenset(),
            full_suite=False,
        ),
    )

    def _fake_run_pytest(targets: list[str], *, marker: str, use_cov: bool = False) -> int:
        pytest_calls.append((targets, marker))
        return 1

    monkeypatch.setattr("scripts.helpers.ci_gate.main._run_pytest", _fake_run_pytest)

    assert main() == 1
    assert pytest_calls == [
        (
            ["tests/regression/cli/test_old.py::test_old"],
            "not npu and not nightly and not network",
        )
    ]


def test_main_uses_full_suite_targets_when_config_changes(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
    baseline: Baseline,
) -> None:
    pytest_calls: list[tuple[list[str], str]] = []

    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.load_baseline", lambda *_args: baseline)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.fetch_diff", lambda *_args: _empty_diff())
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.classify_changes",
        lambda *_args: ChangeSet.build(config=("pyproject.toml",)),
    )
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.build_ci_gate_plan",
        lambda *_args, **_kwargs: CiGatePlan(
            blocking_errors=(),
            deleted_source_tests=frozenset(),
            changed_test_nodes=frozenset(),
            regression_tests=frozenset(),
            full_suite=True,
        ),
    )

    def _fake_run_pytest(targets: list[str], *, marker: str, use_cov: bool = False) -> int:
        pytest_calls.append((targets, marker))
        return 0

    monkeypatch.setattr("scripts.helpers.ci_gate.main._run_pytest", _fake_run_pytest)

    assert main() == 0
    assert pytest_calls == [(["tests"], "not npu")]


def test_main_skips_exempt_regression_targets(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
    baseline: Baseline,
) -> None:
    pytest_calls: list[list[str]] = []

    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    exempt_baseline = baseline.__class__(
        test_map=baseline.test_map,
        exemptions=baseline.exemptions,
        test_exemptions=(
            TestExemption(
                test_id="tests/regression/cli/test_new.py::test_new",
                reason="x",
                applicant="a",
                approver="fangkai",
                deadline=date(2099, 12, 31),
            ),
        ),
        discovery=baseline.discovery,
        roots=baseline.roots,
    )
    monkeypatch.setattr("scripts.helpers.ci_gate.main.load_baseline", lambda *_args: exempt_baseline)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.fetch_diff", lambda *_args: _empty_diff())
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.classify_changes",
        lambda *_args: ChangeSet.build(modified_source={"cli/main.py": frozenset({1})}),
    )
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.build_ci_gate_plan",
        lambda *_args, **_kwargs: CiGatePlan(
            blocking_errors=(),
            deleted_source_tests=frozenset(),
            changed_test_nodes=frozenset(),
            regression_tests=frozenset({"tests/regression/cli/test_new.py::test_new"}),
            full_suite=False,
        ),
    )

    def _fake_run_pytest(targets: list[str], *, marker: str, use_cov: bool = False) -> int:
        pytest_calls.append(targets)
        return 0

    monkeypatch.setattr("scripts.helpers.ci_gate.main._run_pytest", _fake_run_pytest)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.build_coverage_mapping_errors", lambda *_args, **_kwargs: ())

    assert main() == 0
    assert pytest_calls == []


def test_main_runs_union_targets_when_available(
    monkeypatch: pytest.MonkeyPatch,
    gate_cfg: Config,
    baseline: Baseline,
) -> None:
    pytest_calls: list[tuple[list[str], str]] = []

    monkeypatch.setattr("scripts.helpers.ci_gate.main.Config.from_env", lambda: gate_cfg)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.setup_logger", lambda: logging.getLogger("ci_gate"))
    monkeypatch.setattr("scripts.helpers.ci_gate.main.log_env_audit", lambda *_args, **_kwargs: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.resolve_base_ref", lambda *_args: "abc" * 10)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.validate_gate_policy_if_changed", lambda *_args: None)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.load_baseline", lambda *_args: baseline)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.fetch_diff", lambda *_args: _empty_diff())
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.classify_changes",
        lambda *_args: ChangeSet.build(modified_source={"cli/main.py": frozenset({1})}),
    )
    monkeypatch.setattr(
        "scripts.helpers.ci_gate.main.build_ci_gate_plan",
        lambda *_args, **_kwargs: CiGatePlan(
            blocking_errors=(),
            deleted_source_tests=frozenset(),
            changed_test_nodes=frozenset(),
            regression_tests=frozenset({"tests/regression/cli/test_new.py::test_new"}),
            full_suite=False,
        ),
    )

    def _fake_run_pytest(targets: list[str], *, marker: str, use_cov: bool = False) -> int:
        pytest_calls.append((targets, marker))
        return 0

    monkeypatch.setattr("scripts.helpers.ci_gate.main._run_pytest", _fake_run_pytest)
    monkeypatch.setattr("scripts.helpers.ci_gate.main.build_coverage_mapping_errors", lambda *_args, **_kwargs: ())

    assert main() == 0
    assert pytest_calls == [
        (
            ["tests/regression/cli/test_new.py::test_new"],
            "not npu and not nightly and not network",
        )
    ]