"""Tests for promptkit.tester module."""

import json
from pathlib import Path
from typing import Dict, List

import pytest

from promptkit.prompt import Prompt, PromptVersion
from promptkit.tester import (
    PromptTester,
    TestCase,
    TestReport,
    TestResult,
    TestStatus,
    load_test_cases,
)


# ── Enum & dataclass tests ──────────────────────────────────────


class TestTestStatus:
    """Tests for TestStatus enum."""

    def test_values(self) -> None:
        assert TestStatus.PASSED.value == "passed"
        assert TestStatus.FAILED.value == "failed"
        assert TestStatus.SKIPPED.value == "skipped"
        assert TestStatus.ERROR.value == "error"


class TestTestCase:
    """Tests for TestCase dataclass."""

    def test_to_dict(self) -> None:
        tc = TestCase(
            name="tc1",
            description="desc",
            variables={"x": "1"},
            assertions=[{"type": "contains", "value": "hello"}],
            tags=["smoke"],
        )
        d = tc.to_dict()
        assert d["name"] == "tc1"
        assert d["variables"] == {"x": "1"}

    def test_from_dict(self) -> None:
        data = {"name": "tc1", "variables": {"a": "b"}, "assertions": []}
        tc = TestCase.from_dict(data)
        assert tc.name == "tc1"
        assert tc.variables == {"a": "b"}

    def test_from_dict_defaults(self) -> None:
        tc = TestCase.from_dict({"name": "minimal"})
        assert tc.description == ""
        assert tc.variables == {}
        assert tc.assertions == []
        assert tc.tags == []


class TestTestResult:
    """Tests for TestResult dataclass."""

    def test_to_dict(self) -> None:
        r = TestResult(test_name="t1", status=TestStatus.PASSED, message="ok")
        d = r.to_dict()
        assert d["status"] == "passed"
        assert d["test_name"] == "t1"


class TestTestReport:
    """Tests for TestReport properties."""

    def _make_report(self, statuses: List[TestStatus]) -> TestReport:
        report = TestReport(prompt_name="test", version=1)
        for i, s in enumerate(statuses):
            report.results.append(TestResult(test_name=f"t{i}", status=s))
        return report

    def test_all_passed(self) -> None:
        report = self._make_report([TestStatus.PASSED, TestStatus.PASSED])
        assert report.all_passed is True
        assert report.passed == 2
        assert report.total == 2

    def test_some_failed(self) -> None:
        report = self._make_report([TestStatus.PASSED, TestStatus.FAILED])
        assert report.all_passed is False
        assert report.failed == 1

    def test_errors_count(self) -> None:
        report = self._make_report([TestStatus.ERROR, TestStatus.ERROR])
        assert report.errors == 2
        assert report.all_passed is False

    def test_skipped_count(self) -> None:
        report = self._make_report([TestStatus.SKIPPED])
        assert report.skipped == 1

    def test_to_dict(self) -> None:
        report = self._make_report([TestStatus.PASSED])
        d = report.to_dict()
        assert d["summary"]["total"] == 1
        assert d["all_passed"] is True


# ── PromptTester assertion tests ─────────────────────────────────


def _make_prompt(content: str) -> Prompt:
    """Helper to create a single-version prompt."""
    p = Prompt(name="test")
    p.add_version(content)
    return p


class TestAssertionContains:
    """Tests for 'contains' assertion."""

    def test_contains_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "contains", "value": "world"}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_contains_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "contains", "value": "missing"}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionNotContains:
    """Tests for 'not_contains' assertion."""

    def test_not_contains_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "not_contains", "value": "bye"}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_not_contains_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "not_contains", "value": "Hello"}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionStartsEndsWith:
    """Tests for 'starts_with' and 'ends_with' assertions."""

    def test_starts_with_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "starts_with", "value": "Hello"}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_starts_with_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "starts_with", "value": "world"}])
        report = tester.test(p, [tc])
        assert not report.all_passed

    def test_ends_with_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "ends_with", "value": "world"}])
        report = tester.test(p, [tc])
        assert report.all_passed


class TestAssertionRegex:
    """Tests for 'matches_regex' assertion."""

    def test_regex_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world 123")
        tc = TestCase(name="t", assertions=[{"type": "matches_regex", "value": r"\d+"}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_regex_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "matches_regex", "value": r"^\d+$"}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionLength:
    """Tests for min_length / max_length assertions."""

    def test_min_length_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "min_length", "value": 5}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_min_length_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hi")
        tc = TestCase(name="t", assertions=[{"type": "min_length", "value": 100}])
        report = tester.test(p, [tc])
        assert not report.all_passed

    def test_max_length_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Short")
        tc = TestCase(name="t", assertions=[{"type": "max_length", "value": 100}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_max_length_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("x" * 200)
        tc = TestCase(name="t", assertions=[{"type": "max_length", "value": 50}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionHasVariable:
    """Tests for 'has_variable' assertion."""

    def test_has_variable_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello {{name}}")
        tc = TestCase(name="t", assertions=[{"type": "has_variable", "value": "name"}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_has_variable_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello world")
        tc = TestCase(name="t", assertions=[{"type": "has_variable", "value": "name"}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionRendersWithoutError:
    """Tests for 'renders_without_error' assertion."""

    def test_renders_ok(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello {{name}}")
        tc = TestCase(
            name="t",
            variables={"name": "Alice"},
            assertions=[{"type": "renders_without_error", "value": None}],
        )
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_renders_plain_text(self) -> None:
        tester = PromptTester()
        p = _make_prompt("No variables here")
        tc = TestCase(name="t", assertions=[{"type": "renders_without_error", "value": None}])
        report = tester.test(p, [tc])
        assert report.all_passed


class TestAssertionTokenCount:
    """Tests for 'token_count_below' assertion."""

    def test_token_count_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Short text")
        tc = TestCase(name="t", assertions=[{"type": "token_count_below", "value": 1000}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_token_count_fail(self) -> None:
        tester = PromptTester()
        p = _make_prompt("x" * 10000)
        tc = TestCase(name="t", assertions=[{"type": "token_count_below", "value": 10}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionNoPII:
    """Tests for 'no_pii' assertion."""

    def test_no_pii_pass(self) -> None:
        tester = PromptTester()
        p = _make_prompt("You are a helpful assistant.")
        tc = TestCase(name="t", assertions=[{"type": "no_pii", "value": None}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_pii_email_detected(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Contact me at user@example.com for help.")
        tc = TestCase(name="t", assertions=[{"type": "no_pii", "value": None}])
        report = tester.test(p, [tc])
        assert not report.all_passed

    def test_pii_ssn_detected(self) -> None:
        tester = PromptTester()
        p = _make_prompt("My SSN is 123-45-6789. Please help.")
        tc = TestCase(name="t", assertions=[{"type": "no_pii", "value": None}])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestAssertionWordCount:
    """Tests for 'word_count_between' assertion."""

    def test_word_count_in_range(self) -> None:
        tester = PromptTester()
        p = _make_prompt("one two three four five")
        tc = TestCase(name="t", assertions=[
            {"type": "word_count_between", "value": {"min": 3, "max": 10}}
        ])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_word_count_below_range(self) -> None:
        tester = PromptTester()
        p = _make_prompt("one")
        tc = TestCase(name="t", assertions=[
            {"type": "word_count_between", "value": {"min": 5, "max": 10}}
        ])
        report = tester.test(p, [tc])
        assert not report.all_passed


class TestPromptTesterIntegration:
    """Integration tests for PromptTester."""

    def test_multiple_assertions(self) -> None:
        tester = PromptTester()
        p = _make_prompt("You are a helpful assistant. Please respond clearly.")
        tc = TestCase(name="multi", assertions=[
            {"type": "contains", "value": "helpful"},
            {"type": "starts_with", "value": "You are"},
            {"type": "min_length", "value": 10},
            {"type": "no_pii", "value": None},
        ])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_no_versions_gives_error(self) -> None:
        tester = PromptTester()
        p = Prompt(name="empty")
        tc = TestCase(name="t", assertions=[{"type": "contains", "value": "x"}])
        report = tester.test(p, [tc])
        assert report.errors >= 1

    def test_test_content(self) -> None:
        tester = PromptTester()
        tc = TestCase(name="t", assertions=[{"type": "contains", "value": "world"}])
        report = tester.test_content("Hello world", [tc])
        assert report.all_passed

    def test_variable_rendering_in_test(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello {{name}}")
        tc = TestCase(
            name="t",
            variables={"name": "Alice"},
            assertions=[{"type": "contains", "value": "Alice"}],
        )
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_render_with_wrong_variables_runs_assertions(self) -> None:
        tester = PromptTester()
        p = _make_prompt("Hello {{name}}")
        tc = TestCase(
            name="t",
            variables={"wrong": "value"},
            assertions=[{"type": "contains", "value": "x"}],
        )
        report = tester.test(p, [tc])
        # Jinja2 renders undefined as empty; assertions still run, may fail
        assert report.results[0].status in (TestStatus.FAILED, TestStatus.ERROR)

    def test_unknown_assertion_type(self) -> None:
        tester = PromptTester()
        p = _make_prompt("content")
        tc = TestCase(name="t", assertions=[{"type": "nonexistent_type", "value": "x"}])
        report = tester.test(p, [tc])
        assert not report.all_passed

    def test_custom_assertion(self) -> None:
        tester = PromptTester()
        tester.register_assertion(
            "is_uppercase",
            lambda raw, rendered, value, _vars: (
                (True, "") if rendered == rendered.upper() else (False, "Not uppercase")
            ),
        )
        p = _make_prompt("HELLO WORLD")
        tc = TestCase(name="t", assertions=[{"type": "is_uppercase", "value": None}])
        report = tester.test(p, [tc])
        assert report.all_passed

    def test_duration_tracked(self) -> None:
        tester = PromptTester()
        p = _make_prompt("content")
        tc = TestCase(name="t", assertions=[{"type": "contains", "value": "content"}])
        report = tester.test(p, [tc])
        assert report.total_duration_ms >= 0
        assert report.results[0].duration_ms >= 0


# ── load_test_cases tests ───────────────────────────────────────


class TestLoadTestCases:
    """Tests for the load_test_cases helper."""

    def test_load_json(self, sample_test_cases_json: Path) -> None:
        cases = load_test_cases(sample_test_cases_json)
        assert len(cases) == 2
        assert cases[0].name == "contains_greeting"

    def test_load_yaml(self, sample_test_cases_yaml: Path) -> None:
        cases = load_test_cases(sample_test_cases_yaml)
        assert len(cases) == 2
        assert cases[0].name == "starts_with_check"

    def test_load_nonexistent_raises(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            load_test_cases(tmp_path / "does_not_exist.json")

    def test_load_unsupported_format(self, tmp_path: Path) -> None:
        f = tmp_path / "tests.xml"
        f.write_text("<tests></tests>")
        with pytest.raises(ValueError, match="Unsupported"):
            load_test_cases(f)

    def test_load_json_list_format(self, tmp_path: Path) -> None:
        data = [
            {"name": "t1", "assertions": [{"type": "contains", "value": "hi"}]},
        ]
        f = tmp_path / "list.json"
        f.write_text(json.dumps(data))
        cases = load_test_cases(f)
        assert len(cases) == 1
        assert cases[0].name == "t1"
