"""Tests for failure taxonomy library."""
from __future__ import annotations

import json
from pathlib import Path

import pytest

from abi_evals.failure_taxonomy import FailureTaxonomy, FailureType


class TestFailureType:
    """Tests for FailureType dataclass."""

    def test_creation(self):
        """Test creating a FailureType instance."""
        ft = FailureType(
            failure_id="FT-001",
            name="test_failure",
            category="correctness",
            severity="high",
            description="A test failure",
            detection_probes=["error", "failed"],
            mitigation="Fix the issue"
        )
        assert ft.failure_id == "FT-001"
        assert ft.name == "test_failure"
        assert ft.category == "correctness"
        assert ft.severity == "high"
        assert ft.description == "A test failure"
        assert ft.detection_probes == ["error", "failed"]
        assert ft.mitigation == "Fix the issue"

    def test_creation_with_defaults(self):
        """Test creating a FailureType with default values."""
        ft = FailureType(
            failure_id="FT-002",
            name="test_failure",
            category="performance",
            severity="medium",
            description="Another test failure"
        )
        assert ft.detection_probes == []
        assert ft.mitigation == ""


class TestFailureTaxonomy:
    """Tests for FailureTaxonomy class."""

    def test_empty_taxonomy(self):
        """Test creating an empty taxonomy."""
        taxonomy = FailureTaxonomy()
        assert len(taxonomy) == 0
        assert taxonomy.types == {}

    def test_add_failure_type(self):
        """Test adding failure types to the taxonomy."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="test_failure",
            category="correctness",
            severity="high",
            description="A test failure"
        )
        taxonomy.add(ft)
        assert len(taxonomy) == 1
        assert taxonomy.get("FT-001") == ft

    def test_get_existing(self):
        """Test getting an existing failure type."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="test_failure",
            category="correctness",
            severity="high",
            description="A test failure"
        )
        taxonomy.add(ft)
        result = taxonomy.get("FT-001")
        assert result is not None
        assert result.failure_id == "FT-001"

    def test_get_nonexistent(self):
        """Test getting a non-existent failure type."""
        taxonomy = FailureTaxonomy()
        result = taxonomy.get("FT-999")
        assert result is None

    def test_by_category(self):
        """Test filtering by category."""
        taxonomy = FailureTaxonomy()
        ft1 = FailureType(
            failure_id="FT-001",
            name="correctness_failure",
            category="correctness",
            severity="high",
            description="A correctness failure"
        )
        ft2 = FailureType(
            failure_id="FT-002",
            name="performance_failure",
            category="performance",
            severity="medium",
            description="A performance failure"
        )
        ft3 = FailureType(
            failure_id="FT-003",
            name="another_correctness_failure",
            category="correctness",
            severity="low",
            description="Another correctness failure"
        )
        taxonomy.add(ft1)
        taxonomy.add(ft2)
        taxonomy.add(ft3)

        correctness_failures = taxonomy.by_category("correctness")
        assert len(correctness_failures) == 2
        assert all(f.category == "correctness" for f in correctness_failures)

        performance_failures = taxonomy.by_category("performance")
        assert len(performance_failures) == 1
        assert performance_failures[0].failure_id == "FT-002"

        no_failures = taxonomy.by_category("governance")
        assert len(no_failures) == 0

    def test_by_severity(self):
        """Test filtering by severity."""
        taxonomy = FailureTaxonomy()
        ft1 = FailureType(
            failure_id="FT-001",
            name="critical_failure",
            category="correctness",
            severity="critical",
            description="A critical failure"
        )
        ft2 = FailureType(
            failure_id="FT-002",
            name="high_failure",
            category="performance",
            severity="high",
            description="A high severity failure"
        )
        ft3 = FailureType(
            failure_id="FT-003",
            name="another_critical_failure",
            category="governance",
            severity="critical",
            description="Another critical failure"
        )
        taxonomy.add(ft1)
        taxonomy.add(ft2)
        taxonomy.add(ft3)

        critical_failures = taxonomy.by_severity("critical")
        assert len(critical_failures) == 2
        assert all(f.severity == "critical" for f in critical_failures)

        high_failures = taxonomy.by_severity("high")
        assert len(high_failures) == 1
        assert high_failures[0].failure_id == "FT-002"

        no_failures = taxonomy.by_severity("low")
        assert len(no_failures) == 0

    def test_classify_single_match(self):
        """Test classifying an error with a single matching probe."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="file_not_found",
            category="correctness",
            severity="high",
            description="File not found error",
            detection_probes=["FileNotFoundError", "no such file"]
        )
        taxonomy.add(ft)

        error_text = "Error: FileNotFoundError at line 42"
        matches = taxonomy.classify(error_text)
        assert len(matches) == 1
        assert matches[0].failure_id == "FT-001"

    def test_classify_multiple_matches(self):
        """Test classifying an error that matches multiple failure types."""
        taxonomy = FailureTaxonomy()
        ft1 = FailureType(
            failure_id="FT-001",
            name="file_error",
            category="correctness",
            severity="high",
            description="File error",
            detection_probes=["file not found"]
        )
        ft2 = FailureType(
            failure_id="FT-002",
            name="import_error",
            category="correctness",
            severity="high",
            description="Import error",
            detection_probes=["import error", "module not found"]
        )
        taxonomy.add(ft1)
        taxonomy.add(ft2)

        error_text = "Import error: module not found, file not found"
        matches = taxonomy.classify(error_text)
        assert len(matches) == 2
        assert {m.failure_id for m in matches} == {"FT-001", "FT-002"}

    def test_classify_no_matches(self):
        """Test classifying an error with no matching probes."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="file_error",
            category="correctness",
            severity="high",
            description="File error",
            detection_probes=["FileNotFoundError"]
        )
        taxonomy.add(ft)

        error_text = "Some other error occurred"
        matches = taxonomy.classify(error_text)
        assert len(matches) == 0

    def test_classify_case_insensitive(self):
        """Test that classification is case-insensitive."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="file_error",
            category="correctness",
            severity="high",
            description="File error",
            detection_probes=["FileNotFoundError"]
        )
        taxonomy.add(ft)

        error_text = "Error: filenotfounderror occurred"
        matches = taxonomy.classify(error_text)
        assert len(matches) == 1
        assert matches[0].failure_id == "FT-001"

    def test_classify_multiple_probes_same_type(self):
        """Test that a failure type is only added once even if multiple probes match."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="file_error",
            category="correctness",
            severity="high",
            description="File error",
            detection_probes=["file not found", "no such file", "FileNotFoundError"]
        )
        taxonomy.add(ft)

        error_text = "FileNotFoundError: no such file or directory, file not found"
        matches = taxonomy.classify(error_text)
        assert len(matches) == 1  # Should only match once
        assert matches[0].failure_id == "FT-001"

    def test_to_dict(self):
        """Test converting taxonomy to dictionary."""
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="test_failure",
            category="correctness",
            severity="high",
            description="A test failure",
            detection_probes=["error"],
            mitigation="Fix it"
        )
        taxonomy.add(ft)

        result = taxonomy.to_dict()
        assert "failures" in result
        assert len(result["failures"]) == 1
        assert result["failures"][0]["failure_id"] == "FT-001"
        assert result["failures"][0]["name"] == "test_failure"
        assert result["failures"][0]["category"] == "correctness"
        assert result["failures"][0]["severity"] == "high"
        assert result["failures"][0]["description"] == "A test failure"
        assert result["failures"][0]["detection_probes"] == ["error"]
        assert result["failures"][0]["mitigation"] == "Fix it"


class TestLoadingFromFiles:
    """Tests for loading taxonomy from files."""

    def test_load_from_yaml_builtin(self, tmp_path):
        """Test loading from YAML file."""
        pytest.importorskip("yaml")

        yaml_content = """
failures:
  - failure_id: FT-001
    name: scope_expansion
    category: correctness
    severity: medium
    description: Agent adds features not requested
    detection_probes:
      - unrequested feature
      - scope expansion
    mitigation: Strengthen prompt constraints

  - failure_id: FT-002
    name: hallucinated_paths
    category: correctness
    severity: high
    description: Agent references files that don't exist
    detection_probes:
      - file not found
      - FileNotFoundError
    mitigation: Provide explicit context
"""
        yaml_file = tmp_path / "test_taxonomy.yaml"
        yaml_file.write_text(yaml_content)

        taxonomy = FailureTaxonomy.from_yaml(yaml_file)
        assert len(taxonomy) == 2
        assert taxonomy.get("FT-001") is not None
        assert taxonomy.get("FT-002") is not None
        assert taxonomy.get("FT-001").name == "scope_expansion"
        assert taxonomy.get("FT-002").severity == "high"

    def test_load_from_yaml_not_found(self):
        """Test loading from non-existent YAML file."""
        pytest.importorskip("yaml")

        with pytest.raises(FileNotFoundError):
            FailureTaxonomy.from_yaml("nonexistent.yaml")

    def test_load_from_json(self, tmp_path):
        """Test loading from JSON file."""
        json_content = {
            "failures": [
                {
                    "failure_id": "FT-001",
                    "name": "scope_expansion",
                    "category": "correctness",
                    "severity": "medium",
                    "description": "Agent adds features not requested",
                    "detection_probes": ["unrequested feature", "scope expansion"],
                    "mitigation": "Strengthen prompt constraints"
                },
                {
                    "failure_id": "FT-002",
                    "name": "hallucinated_paths",
                    "category": "correctness",
                    "severity": "high",
                    "description": "Agent references files that don't exist",
                    "detection_probes": ["file not found", "FileNotFoundError"],
                    "mitigation": "Provide explicit context"
                }
            ]
        }
        json_file = tmp_path / "test_taxonomy.json"
        json_file.write_text(json.dumps(json_content))

        taxonomy = FailureTaxonomy.from_json(json_file)
        assert len(taxonomy) == 2
        assert taxonomy.get("FT-001") is not None
        assert taxonomy.get("FT-002") is not None
        assert taxonomy.get("FT-001").name == "scope_expansion"
        assert taxonomy.get("FT-002").severity == "high"

    def test_load_from_json_not_found(self):
        """Test loading from non-existent JSON file."""
        with pytest.raises(FileNotFoundError):
            FailureTaxonomy.from_json("nonexistent.json")

    def test_load_builtin_taxonomy(self):
        """Test loading the built-in failure taxonomy."""
        pytest.importorskip("yaml")

        # Get the path to the data directory
        project_root = Path(__file__).parent.parent
        yaml_path = project_root / "data" / "failure_taxonomy.yaml"

        if not yaml_path.exists():
            pytest.skip("Built-in taxonomy file not found")

        taxonomy = FailureTaxonomy.from_yaml(yaml_path)

        # Verify expected failures are present
        assert len(taxonomy) >= 8  # At least the 8 core failures

        # Check specific failures
        ft001 = taxonomy.get("FT-001")
        assert ft001 is not None
        assert ft001.name == "scope_expansion"
        assert ft001.category == "correctness"
        assert ft001.severity == "medium"

        ft002 = taxonomy.get("FT-002")
        assert ft002 is not None
        assert ft002.name == "hallucinated_paths"
        assert ft002.severity == "high"

        ft004 = taxonomy.get("FT-004")
        assert ft004 is not None
        assert ft004.severity == "critical"

    def test_classify_with_builtin_taxonomy(self):
        """Test classify() with the built-in taxonomy."""
        pytest.importorskip("yaml")

        project_root = Path(__file__).parent.parent
        yaml_path = project_root / "data" / "failure_taxonomy.yaml"

        if not yaml_path.exists():
            pytest.skip("Built-in taxonomy file not found")

        taxonomy = FailureTaxonomy.from_yaml(yaml_path)

        # Test hallucinated paths detection
        error1 = "FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.py'"
        matches1 = taxonomy.classify(error1)
        assert len(matches1) > 0
        assert any(m.failure_id == "FT-002" for m in matches1)

        # Test context overflow detection
        error2 = "Error: context length exceeded maximum of 200000 tokens"
        matches2 = taxonomy.classify(error2)
        assert len(matches2) > 0
        assert any(m.failure_id == "FT-006" for m in matches2)

        # Test schema drift detection
        error3 = "ValidationError: missing required field 'name'"
        matches3 = taxonomy.classify(error3)
        assert len(matches3) > 0
        assert any(m.failure_id == "FT-003" for m in matches3)


class TestValidation:
    """Tests for schema validation."""

    def test_validate_against_schema(self, tmp_path):
        """Test validating taxonomy against JSON schema."""
        jsonschema = pytest.importorskip("jsonschema")

        # Create a valid taxonomy
        taxonomy = FailureTaxonomy()
        ft = FailureType(
            failure_id="FT-001",
            name="test_failure",
            category="correctness",
            severity="high",
            description="A test failure with sufficient description length",
            detection_probes=["error"],
            mitigation="Fix it"
        )
        taxonomy.add(ft)

        # Create a schema
        schema = {
            "$schema": "http://json-schema.org/draft-07/schema#",
            "type": "object",
            "properties": {
                "failures": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "failure_id": {"type": "string", "pattern": "^FT-\\d{3,}$"},
                            "name": {"type": "string"},
                            "category": {"type": "string"},
                            "severity": {"type": "string"},
                            "description": {"type": "string", "minLength": 10},
                            "detection_probes": {"type": "array"},
                            "mitigation": {"type": "string"}
                        },
                        "required": ["failure_id", "name", "category", "severity", "description"]
                    }
                }
            },
            "required": ["failures"]
        }

        schema_file = tmp_path / "test_schema.json"
        schema_file.write_text(json.dumps(schema))

        is_valid, errors = taxonomy.validate_against_schema(schema_file)
        assert is_valid
        assert len(errors) == 0

    def test_builtin_taxonomy_validates(self):
        """Test that the built-in taxonomy validates against its schema."""
        pytest.importorskip("yaml")
        pytest.importorskip("jsonschema")

        project_root = Path(__file__).parent.parent
        yaml_path = project_root / "data" / "failure_taxonomy.yaml"
        schema_path = project_root / "data" / "failure_taxonomy.schema.json"

        if not yaml_path.exists() or not schema_path.exists():
            pytest.skip("Built-in taxonomy or schema file not found")

        taxonomy = FailureTaxonomy.from_yaml(yaml_path)
        is_valid, errors = taxonomy.validate_against_schema(schema_path)

        assert is_valid, f"Validation errors: {errors}"
        assert len(errors) == 0
