"""Tests for loom.validator — rule-based task-to-codebase matching engine.

Covers:
  - Each rule independently (FileExistenceRule, SymbolExistenceRule, KeywordPatternRule)
  - Threshold boundary conditions with parametrize
  - Aggregation (max confidence wins)
  - Edge cases: empty snapshot, short symbols, mutation verbs, normalisation
"""

from __future__ import annotations

import pytest

from loom.scanner import CodebaseSummary, FileSymbols
from loom.validator import (
    FileExistenceRule,
    KeywordPatternRule,
    MatchResult,
    SymbolExistenceRule,
    ValidationStatus,
    ValidatorConfig,
    classify_match,
    match_task_to_codebase,
    normalise,
)


# ── Helpers ──────────────────────────────────────────────────────────


def _summary(*file_specs: tuple[str, list[str], list[str]]) -> CodebaseSummary:
    """Build a CodebaseSummary from (path, functions, classes) tuples."""
    files = [
        FileSymbols(path=path, functions=funcs, classes=classes)
        for path, funcs, classes in file_specs
    ]
    return CodebaseSummary(root="/tmp/project", files=files)


SAMPLE_SUMMARY = _summary(
    ("loom/graph/store.py", ["create_task", "get_task", "update_task"], ["TaskStore"]),
    ("loom/graph/cache.py", ["sync_task", "get_cached_task"], ["CacheManager"]),
    ("loom/cli.py", ["main", "parse_args"], []),
    ("tests/test_store.py", ["test_create", "test_get"], []),
)


# ── Tests: normalise ─────────────────────────────────────────────────


class TestNormalise:
    """String normalisation: lowercase, camelCase split, stopword removal."""

    def test_basic_lowercase(self) -> None:
        assert normalise("Hello World") == ["hello", "world"]

    def test_camel_case_split(self) -> None:
        result = normalise("createUserProfile")
        assert result == ["create", "user", "profile"]

    def test_pascal_case_split(self) -> None:
        result = normalise("TaskStore")
        assert result == ["task", "store"]

    def test_stopword_removal(self) -> None:
        result = normalise("Create a new task for the user")
        assert "a" not in result
        assert "the" not in result
        assert "for" not in result
        assert "create" in result
        assert "new" in result
        assert "task" in result
        assert "user" in result

    def test_non_alpha_splitting(self) -> None:
        result = normalise("loom/graph/store.py")
        assert "loom" in result
        assert "graph" in result
        assert "store" in result
        assert "py" in result

    def test_empty_string(self) -> None:
        assert normalise("") == []

    def test_only_stopwords(self) -> None:
        assert normalise("the a an") == []

    def test_mixed_delimiters(self) -> None:
        result = normalise("create_task-handler.v2")
        assert result == ["create", "task", "handler", "v2"]


# ── Tests: FileExistenceRule ──────────────────────────────────────────


class TestFileExistenceRule:
    """FileExistenceRule matches file paths or stems in task text."""

    def setup_method(self) -> None:
        self.rule = FileExistenceRule()

    def test_exact_path_match(self) -> None:
        task = {"title": "Update loom/graph/store.py to add batching"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence == 1.0
        assert result.matched_artifact == "loom/graph/store.py"
        assert result.rule_name == "file_existence"

    def test_stem_match(self) -> None:
        task = {"title": "Fix the store module error handling"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence == 0.75
        assert "store" in result.matched_artifact

    def test_no_match(self) -> None:
        task = {"title": "Deploy to production server"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is None

    def test_empty_summary_returns_none(self) -> None:
        empty = CodebaseSummary(root="/tmp", files=[])
        task = {"title": "Update store.py"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, empty)

        assert result is None

    def test_short_stem_ignored(self) -> None:
        """Files with very short stems (< 3 chars) are skipped to avoid false positives."""
        summary = _summary(("x.py", ["f"], []))
        task = {"title": "Fix x issues"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, summary)

        assert result is None

    def test_context_files_searched(self) -> None:
        """Context description is also searched for file paths."""
        task = {
            "title": "Refactor caching",
            "context": {"description": "Modify loom/graph/cache.py for performance"},
        }
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.matched_artifact == "loom/graph/cache.py"


# ── Tests: SymbolExistenceRule ────────────────────────────────────────


class TestSymbolExistenceRule:
    """SymbolExistenceRule matches function/class names in the task."""

    def setup_method(self) -> None:
        self.rule = SymbolExistenceRule()

    def test_exact_symbol_match(self) -> None:
        task = {"title": "Implement create_task function"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence >= 0.85
        assert "create_task" in result.matched_artifact

    def test_class_name_match(self) -> None:
        task = {"title": "Refactor TaskStore class"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert "TaskStore" in result.matched_artifact

    def test_no_symbol_match(self) -> None:
        task = {"title": "Write documentation for API"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        # Should not match any existing symbol
        # (or match with very low confidence via fuzzy)
        if result is not None:
            assert result.confidence < 0.65

    def test_short_symbol_ignored(self) -> None:
        """Symbols shorter than MIN_SYMBOL_LEN are skipped."""
        summary = _summary(("m.py", ["f", "go", "run"], ["A"]))
        task = {"title": "Run the function f"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, summary)

        # "f", "go", "A" are all too short; "run" is only 3 chars
        # None of them should match since they're below MIN_SYMBOL_LEN=4
        assert result is None

    def test_empty_summary_returns_none(self) -> None:
        empty = CodebaseSummary(root="/tmp", files=[])
        task = {"title": "create_task"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, empty)

        assert result is None

    def test_fuzzy_match(self) -> None:
        """A close-but-not-exact symbol name should trigger fuzzy matching."""
        task = {"title": "Update the sync task cache function"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence > 0


# ── Tests: KeywordPatternRule ─────────────────────────────────────────


class TestKeywordPatternRule:
    """KeywordPatternRule matches creation-verb + artifact patterns."""

    def setup_method(self) -> None:
        self.rule = KeywordPatternRule()

    def test_create_verb_matches_existing(self) -> None:
        task = {"title": "Create task store module"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence > 0.5
        assert result.rule_name == "keyword_pattern"

    def test_implement_verb(self) -> None:
        task = {"title": "Implement cache manager"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert "CacheManager" in result.matched_artifact or "cache" in result.matched_artifact.lower()

    def test_non_creation_verb_no_match(self) -> None:
        task = {"title": "Review the code quality"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is None

    def test_empty_summary_returns_none(self) -> None:
        empty = CodebaseSummary(root="/tmp", files=[])
        task = {"title": "Create user handler"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, empty)

        assert result is None

    def test_build_verb(self) -> None:
        task = {"title": "Build task store"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is not None
        assert result.confidence >= 0.5

    def test_no_object_after_verb(self) -> None:
        """A creation verb alone (with only stopwords after) should not match."""
        task = {"title": "Create a the"}
        tokens = normalise(task["title"])

        result = self.rule.evaluate(tokens, task, SAMPLE_SUMMARY)

        assert result is None


# ── Tests: match_task_to_codebase aggregator ──────────────────────────


class TestMatchTaskToCodebase:
    """The aggregator runs all rules and returns the best match."""

    def test_highest_confidence_wins(self) -> None:
        """When multiple rules fire, the one with highest confidence wins."""
        task = {"title": "Update loom/graph/store.py create_task function"}

        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        # FileExistenceRule should give 1.0 for exact path match
        assert result.confidence == 1.0
        assert result.rule_name == "file_existence"

    def test_no_match_returns_zero_confidence(self) -> None:
        task = {"title": "Deploy infrastructure to AWS"}

        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        assert result.confidence == 0.0
        assert result.rule_name == "none"
        assert result.matched_artifact == ""

    def test_empty_summary(self) -> None:
        empty = CodebaseSummary(root="/tmp", files=[])
        task = {"title": "Create something"}

        result = match_task_to_codebase(task, empty)

        assert result.confidence == 0.0

    def test_empty_task_title(self) -> None:
        task = {"title": ""}

        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        assert result.confidence == 0.0

    def test_custom_rules(self) -> None:
        """Custom rules list overrides defaults."""

        class AlwaysMatchRule:
            name = "always_match"

            def evaluate(self, task_tokens, task_dict, summary):
                return MatchResult(
                    rule_name=self.name,
                    confidence=0.99,
                    matched_artifact="everywhere",
                    rationale="Always matches",
                )

        task = {"title": "Anything"}
        result = match_task_to_codebase(
            task, SAMPLE_SUMMARY, rules=[AlwaysMatchRule()]
        )

        assert result.confidence == 0.99
        assert result.rule_name == "always_match"


# ── Tests: classify_match with threshold boundaries ───────────────────


class TestClassifyMatch:
    """Threshold boundary conditions for classification."""

    @pytest.mark.parametrize(
        "confidence, expected",
        [
            (1.0, ValidationStatus.PRE_COMPLETED),
            (0.85, ValidationStatus.PRE_COMPLETED),
            (0.90, ValidationStatus.PRE_COMPLETED),
            (0.84, ValidationStatus.UNCERTAIN),
            (0.65, ValidationStatus.UNCERTAIN),
            (0.70, ValidationStatus.UNCERTAIN),
            (0.64, ValidationStatus.PENDING),
            (0.0, ValidationStatus.PENDING),
            (0.50, ValidationStatus.PENDING),
        ],
    )
    def test_default_thresholds(self, confidence: float, expected: ValidationStatus) -> None:
        result = MatchResult(
            rule_name="test",
            confidence=confidence,
            matched_artifact="x",
            rationale="test",
        )
        assert classify_match(result) == expected

    @pytest.mark.parametrize(
        "confidence, expected",
        [
            (0.95, ValidationStatus.PRE_COMPLETED),
            (0.94, ValidationStatus.UNCERTAIN),
            (0.80, ValidationStatus.UNCERTAIN),
            (0.79, ValidationStatus.PENDING),
        ],
    )
    def test_custom_thresholds(self, confidence: float, expected: ValidationStatus) -> None:
        config = ValidatorConfig(high_threshold=0.95, uncertain_threshold=0.80)
        result = MatchResult(
            rule_name="test",
            confidence=confidence,
            matched_artifact="x",
            rationale="test",
        )
        assert classify_match(result, config) == expected


# ── Tests: edge cases ─────────────────────────────────────────────────


class TestEdgeCases:
    """Miscellaneous edge cases."""

    def test_task_with_context_string(self) -> None:
        """Context can be a string instead of a dict."""
        task = {
            "title": "Fix something",
            "context": "Check loom/graph/store.py",
        }
        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        # File path in context string should be found
        assert result.confidence > 0

    def test_task_with_context_files_list(self) -> None:
        """Context.files as a list of paths."""
        task = {
            "title": "Refactor module",
            "context": {"files": ["loom/graph/cache.py"]},
        }
        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        assert result.confidence > 0

    def test_mutation_verb_define(self) -> None:
        """'define' is a creation verb that should trigger KeywordPatternRule."""
        task = {"title": "Define cache manager class"}
        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        assert result.confidence > 0

    def test_mutation_verb_setup(self) -> None:
        """'setup' is a creation verb."""
        task = {"title": "Setup task store"}
        result = match_task_to_codebase(task, SAMPLE_SUMMARY)

        assert result.confidence > 0

    def test_validator_config_defaults(self) -> None:
        config = ValidatorConfig()
        assert config.high_threshold == 0.85
        assert config.uncertain_threshold == 0.65

    def test_validation_status_values(self) -> None:
        assert ValidationStatus.PENDING == "pending"
        assert ValidationStatus.PRE_COMPLETED == "pre_completed"
        assert ValidationStatus.UNCERTAIN == "uncertain"

    def test_match_result_is_frozen(self) -> None:
        """MatchResult is immutable (frozen dataclass)."""
        result = MatchResult(
            rule_name="test",
            confidence=0.5,
            matched_artifact="x",
            rationale="y",
        )
        with pytest.raises(AttributeError):
            result.confidence = 0.9  # type: ignore[misc]
