"""Tests for eapctf.ctf.validate — CTF compliance checker."""

from __future__ import annotations

import textwrap
from pathlib import Path

import numpy as np
import pandas as pd
import pytest

from eapctf.ctf.validate import CheckResult, ValidationResult, validate


# ---------------------------------------------------------------------------
# ValidationResult helpers
# ---------------------------------------------------------------------------


class TestValidationResult:
    def test_passed_when_no_fails(self) -> None:
        checks = [
            CheckResult(rule=1, name="test", severity="PASS", detail=""),
            CheckResult(rule=2, name="test2", severity="WARNING", detail=""),
        ]
        vr = ValidationResult(checks=checks)
        assert vr.passed is True

    def test_failed_when_any_auto_fail(self) -> None:
        checks = [
            CheckResult(rule=1, name="test", severity="PASS", detail=""),
            CheckResult(rule=5, name="no-main", severity="FAIL", detail="no main", auto=True),
        ]
        vr = ValidationResult(checks=checks)
        assert vr.passed is False

    def test_manual_fail_does_not_block_passed(self) -> None:
        checks = [
            CheckResult(rule=2, name="manual", severity="FAIL", detail="", auto=False),
        ]
        vr = ValidationResult(checks=checks)
        assert vr.passed is True


# ---------------------------------------------------------------------------
# validate() — Rule 5: main() exists
# ---------------------------------------------------------------------------


class TestRule5MainExists:
    def test_missing_main_fails(self, tmp_path: Path) -> None:
        script = tmp_path / "no_main.py"
        script.write_text("x = 1\n")
        result = validate(script)
        rule5 = next((c for c in result.checks if c.rule == 5), None)
        assert rule5 is not None
        assert rule5.severity == "FAIL"

    def test_main_present_passes(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        rule5 = next((c for c in result.checks if c.rule == 5), None)
        assert rule5 is not None
        assert rule5.severity == "PASS"


# ---------------------------------------------------------------------------
# validate() — Rule 11: main() signature
# ---------------------------------------------------------------------------


class TestRule11Signature:
    def test_correct_signature_passes(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        rule11 = next((c for c in result.checks if c.rule == 11), None)
        assert rule11 is not None
        assert rule11.severity == "PASS"

    def test_wrong_signature_fails(self, tmp_path: Path) -> None:
        script = tmp_path / "bad_sig.py"
        script.write_text(textwrap.dedent("""\
            import pandas as pd

            def main(df: pd.DataFrame) -> pd.DataFrame:
                return df
        """))
        result = validate(script)
        rule11 = next((c for c in result.checks if c.rule == 11), None)
        assert rule11 is not None
        assert rule11.severity == "FAIL"


# ---------------------------------------------------------------------------
# validate() — Rule 12: output schema
# ---------------------------------------------------------------------------


class TestRule12OutputSchema:
    def test_valid_weights_passes(
        self, minimal_model_script: Path, ctf_weights_df: pd.DataFrame
    ) -> None:
        result = validate(minimal_model_script, weights_df=ctf_weights_df)
        rule12 = next((c for c in result.checks if c.rule == 12), None)
        assert rule12 is not None
        assert rule12.severity == "PASS"

    def test_nan_weights_fails(
        self, minimal_model_script: Path, ctf_weights_df: pd.DataFrame
    ) -> None:
        bad = ctf_weights_df.copy()
        bad.loc[0, "w"] = float("nan")
        result = validate(minimal_model_script, weights_df=bad)
        rule12 = next((c for c in result.checks if c.rule == 12), None)
        assert rule12 is not None
        assert rule12.severity == "FAIL"

    def test_missing_column_fails(
        self, minimal_model_script: Path, ctf_weights_df: pd.DataFrame
    ) -> None:
        bad = ctf_weights_df.drop(columns=["w"])
        result = validate(minimal_model_script, weights_df=bad)
        rule12 = next((c for c in result.checks if c.rule == 12), None)
        assert rule12 is not None
        assert rule12.severity == "FAIL"

    def test_duplicate_id_eom_fails(
        self, minimal_model_script: Path, ctf_weights_df: pd.DataFrame
    ) -> None:
        dup = pd.concat([ctf_weights_df.iloc[:1], ctf_weights_df.iloc[:1]], ignore_index=True)
        result = validate(minimal_model_script, weights_df=dup)
        rule12 = next((c for c in result.checks if c.rule == 12), None)
        assert rule12 is not None
        assert rule12.severity == "FAIL"


# ---------------------------------------------------------------------------
# validate() — Rule 10: network isolation
# ---------------------------------------------------------------------------


class TestRule10Network:
    def test_network_import_fails(self, tmp_path: Path) -> None:
        script = tmp_path / "net.py"
        script.write_text(textwrap.dedent("""\
            import requests
            import pandas as pd

            def main(chars, features, daily_ret):
                return pd.DataFrame()
        """))
        result = validate(script)
        rule10 = next((c for c in result.checks if c.rule == 10), None)
        assert rule10 is not None
        assert rule10.severity == "FAIL"

    def test_no_network_passes(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        rule10 = next((c for c in result.checks if c.rule == 10), None)
        assert rule10 is not None
        assert rule10.severity == "PASS"


# ---------------------------------------------------------------------------
# validate() — Rule 13: file constraints
# ---------------------------------------------------------------------------


class TestRule13FileConstraints:
    def test_valid_script_passes(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        rule13 = next((c for c in result.checks if c.rule == 13), None)
        assert rule13 is not None
        assert rule13.severity == "PASS"


# ---------------------------------------------------------------------------
# validate() — Rule 15: prohibited operations
# ---------------------------------------------------------------------------


class TestRule15ProhibitedOps:
    def test_subprocess_flagged(self, tmp_path: Path) -> None:
        script = tmp_path / "sub.py"
        script.write_text(textwrap.dedent("""\
            import subprocess
            import pandas as pd

            def main(chars, features, daily_ret):
                subprocess.run(["ls"])
                return pd.DataFrame()
        """))
        result = validate(script)
        rule15 = next((c for c in result.checks if c.rule == 15), None)
        assert rule15 is not None
        assert rule15.severity in ("FAIL", "WARNING")

    def test_clean_script_passes(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        rule15 = next((c for c in result.checks if c.rule == 15), None)
        assert rule15 is not None
        # Should be PASS or INFO (no prohibited ops)
        assert rule15.severity not in ("FAIL", "WARNING")


# ---------------------------------------------------------------------------
# validate() — integration: non-existent script
# ---------------------------------------------------------------------------


class TestValidateEdgeCases:
    def test_missing_script_raises(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            validate(tmp_path / "nonexistent.py")

    def test_validate_returns_validation_result(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        assert isinstance(result, ValidationResult)
        assert len(result.checks) > 0

    def test_str_output_contains_rules(self, minimal_model_script: Path) -> None:
        result = validate(minimal_model_script)
        text = str(result)
        assert "Rule" in text or "PASS" in text
