"""Tests for TruGuard main class."""

import pytest

from trusys import TruGuard, GuardConfig
from trusys.types import ActionType, ConflictStrategy, ExecutionMode, ScanType
from trusys.exceptions import ConfigurationError, ScanFailedError


class TestTruGuardInitialization:
    """Tests for TruGuard initialization."""

    def test_default_initialization(self):
        guard = TruGuard()
        assert guard.config is not None
        assert guard.config.execution_mode == ExecutionMode.PARALLEL
        assert guard.type == ScanType.INPUT  # Default is input
        assert len(guard.scanners) == 0

    def test_input_initialization(self):
        guard = TruGuard(type="input")
        assert guard.type == ScanType.INPUT

    def test_output_initialization(self):
        guard = TruGuard(type="output")
        assert guard.type == ScanType.OUTPUT

    def test_initialization_with_enum(self):
        guard = TruGuard(type=ScanType.OUTPUT)
        assert guard.type == ScanType.OUTPUT

    def test_invalid_type(self):
        with pytest.raises(ConfigurationError):
            TruGuard(type="invalid")  # type: ignore

    def test_custom_config(self):
        config = GuardConfig(
            execution_mode=ExecutionMode.SEQUENTIAL,
            timeout=10.0,
        )
        guard = TruGuard(type="input", config=config)
        assert guard.config.execution_mode == ExecutionMode.SEQUENTIAL
        assert guard.config.timeout == 10.0


class TestTruGuardConfiguration:
    """Tests for TruGuard configuration."""

    def test_configure_execution_mode(self, guard):
        guard.configure(execution_mode=ExecutionMode.SEQUENTIAL)
        assert guard.config.execution_mode == ExecutionMode.SEQUENTIAL

    def test_configure_execution_mode_string(self, guard):
        guard.configure(execution_mode="pipeline")
        assert guard.config.execution_mode == ExecutionMode.PIPELINE

    def test_configure_timeout(self, guard):
        guard.configure(timeout=5.0)
        assert guard.config.timeout == 5.0

    def test_configure_invalid_timeout(self, guard):
        with pytest.raises(ConfigurationError):
            guard.configure(timeout=-1.0)

    def test_configure_conflict_strategy(self, guard):
        guard.configure(conflict_strategy=ConflictStrategy.FIRST_WINS)
        assert guard.config.conflict_strategy == ConflictStrategy.FIRST_WINS

    def test_configure_method_chaining(self, guard):
        result = guard.configure(timeout=5.0).configure(early_exit=True)
        assert result is guard
        assert guard.config.timeout == 5.0
        assert guard.config.early_exit is True


class TestTruGuardScannerManagement:
    """Tests for scanner management."""

    def test_add_scanners(self, guard, simple_scanner_class):
        scanner = simple_scanner_class()
        guard.add_scanners([scanner])
        assert len(guard.scanners) == 1
        assert guard.scanners[0].name == "simple_test"

    def test_add_scanners_multiple(self, guard, simple_scanner_class, keyword_scanner_class):
        guard.add_scanners([simple_scanner_class(), keyword_scanner_class()])
        assert len(guard.scanners) == 2

    def test_add_scanners_validation_input_guard(self, guard, simple_scanner_class):
        # Create a scanner that only supports output
        from trusys.scanners import BaseScanner, register_scanner
        from trusys.types import ScanContext, ScanResult

        @register_scanner(name="output_only", scan_types=["output"])
        class OutputOnlyScanner(BaseScanner):
            def scan(self, content: dict[str, Any], context):
                return ScanResult(passed=True, scanner_name=self.name, scan_type=context.scan_type)

        # Input guard should reject output-only scanner
        with pytest.raises(ConfigurationError) as exc_info:
            guard.add_scanners([OutputOnlyScanner()])
        assert "does not support input scanning" in str(exc_info.value)

    def test_add_scanners_validation_output_guard(self, simple_scanner_class):
        # Create output guard
        output_guard = TruGuard(type="output")

        # Create a scanner that only supports input
        from trusys.scanners import BaseScanner, register_scanner
        from trusys.types import ScanContext, ScanResult

        @register_scanner(name="input_only", scan_types=["input"])
        class InputOnlyScanner(BaseScanner):
            def scan(self, content: dict[str, Any], context):
                return ScanResult(passed=True, scanner_name=self.name, scan_type=context.scan_type)

        # Output guard should reject input-only scanner
        with pytest.raises(ConfigurationError) as exc_info:
            output_guard.add_scanners([InputOnlyScanner()])
        assert "does not support output scanning" in str(exc_info.value)

    def test_add_scanners_both_types_works_for_input(self, guard, simple_scanner_class):
        # Scanner supports both input and output - should work for input guard
        scanner = simple_scanner_class()
        guard.add_scanners([scanner])
        assert len(guard.scanners) == 1

    def test_add_scanners_both_types_works_for_output(self, simple_scanner_class):
        # Scanner supports both input and output - should work for output guard
        output_guard = TruGuard(type="output")
        scanner = simple_scanner_class()
        output_guard.add_scanners([scanner])
        assert len(output_guard.scanners) == 1

    def test_add_scanners_invalid_type(self, guard):
        with pytest.raises(ConfigurationError):
            guard.add_scanners(["not a scanner"])  # type: ignore

    def test_remove_scanner(self, guard, simple_scanner_class):
        scanner = simple_scanner_class()
        guard.add_scanners([scanner])
        guard.remove_scanner("simple_test")
        assert len(guard.scanners) == 0

    def test_clear_scanners(self, guard, simple_scanner_class, keyword_scanner_class):
        guard.add_scanners([simple_scanner_class(), keyword_scanner_class()])
        guard.clear_scanners()
        assert len(guard.scanners) == 0

    def test_get_scanner(self, guard, simple_scanner_class):
        scanner = simple_scanner_class()
        guard.add_scanners([scanner])
        found = guard.get_scanner("simple_test")
        assert found is scanner

    def test_get_scanner_not_found(self, guard):
        found = guard.get_scanner("nonexistent")
        assert found is None

    def test_method_chaining(self, guard, simple_scanner_class):
        result = guard.add_scanners([simple_scanner_class()])
        assert result is guard


class TestTruGuardScanning:
    """Tests for scanning functionality."""

    def test_scan_no_scanners(self, guard):
        with pytest.raises(ConfigurationError) as exc_info:
            guard.scan(content={"prompt": "test content"})
        assert "No scanners configured" in str(exc_info.value)

    def test_scan_passing(self, guard, simple_scanner_class):
        guard.add_scanners([simple_scanner_class(should_pass=True)])
        result = guard.scan(content={"prompt": "test content"})
        assert result.passed is True
        assert len(result.results) == 1

    def test_scan_failing(self, guard, simple_scanner_class):
        guard.add_scanners([simple_scanner_class(should_pass=False, on_fail="ignore")])
        result = guard.scan(content={"prompt": "test content"})
        assert result.passed is False
        assert len(result.results) == 1

    def test_scan_with_fail_action(self, guard, simple_scanner_class):
        guard.add_scanners([simple_scanner_class(should_pass=False, on_fail="fail")])
        with pytest.raises(ScanFailedError):
            guard.scan(content={"prompt": "test content"})

    def test_scan_input_guard_missing_prompt(self, guard, simple_scanner_class):
        guard.add_scanners([simple_scanner_class()])
        with pytest.raises(ConfigurationError) as exc_info:
            guard.scan(content={})
        assert "prompt" in str(exc_info.value).lower()

    def test_scan_output_guard(self, simple_scanner_class):
        output_guard = TruGuard(type="output")
        output_guard.add_scanners([simple_scanner_class()])
        result = output_guard.scan(content={"prompt": "user input", "response": "test response"})
        assert result.passed is True
        assert len(result.results) == 1

    def test_scan_output_guard_missing_response(self, simple_scanner_class):
        output_guard = TruGuard(type="output")
        output_guard.add_scanners([simple_scanner_class()])
        with pytest.raises(ConfigurationError) as exc_info:
            output_guard.scan(content={"prompt": "user input"})
        assert "response" in str(exc_info.value).lower()


class TestTruGuardWithKeywordScanner:
    """Tests for scanning with keyword scanner."""

    def test_scan_with_keywords(self, guard, keyword_scanner_class):
        guard.add_scanners([keyword_scanner_class(keywords=["bad"], on_fail="ignore")])
        result = guard.scan(content={"prompt": "this is bad content"})
        assert result.passed is False
        assert len(result.all_violations) == 1

    def test_scan_without_keywords(self, guard, keyword_scanner_class):
        guard.add_scanners([keyword_scanner_class(keywords=["bad"])])
        result = guard.scan(content={"prompt": "this is good content"})
        assert result.passed is True


class TestTruGuardWithFixScanner:
    """Tests for scanning with fix scanner."""

    def test_scan_with_fix(self, guard, fix_scanner_class):
        guard.add_scanners([fix_scanner_class(find="bad", replace="good", on_fail="fix")])
        result = guard.scan(content={"prompt": "this is bad content"})
        # With fix action, content is fixed but scan still "fails" (had violations)
        assert result.passed is False
        assert result.final_content is not None
        assert result.final_content == "this is good content"

    def test_scan_fix_not_needed(self, guard, fix_scanner_class):
        guard.add_scanners([fix_scanner_class(find="bad", replace="good")])
        result = guard.scan(content={"prompt": "this is great content"})
        assert result.passed is True
        assert result.final_content is None


class TestTruGuardMultipleScanners:
    """Tests for multiple scanners."""

    def test_multiple_scanners_all_pass(
        self, guard, simple_scanner_class, keyword_scanner_class
    ):
        guard.add_scanners(
            [
                simple_scanner_class(should_pass=True),
                keyword_scanner_class(keywords=["forbidden"]),
            ]
        )
        result = guard.scan(content={"prompt": "safe content"})
        assert result.passed is True
        assert len(result.results) == 2

    def test_multiple_scanners_one_fails(
        self, guard, simple_scanner_class, keyword_scanner_class
    ):
        guard.add_scanners(
            [
                simple_scanner_class(should_pass=True),
                keyword_scanner_class(keywords=["bad"], on_fail="ignore"),
            ]
        )
        result = guard.scan(content={"prompt": "bad content"})
        assert result.passed is False
        assert len(result.results) == 2

    def test_disabled_scanner_skipped(self, guard, simple_scanner_class):
        guard.add_scanners(
            [
                simple_scanner_class(should_pass=True),
                simple_scanner_class(should_pass=False, enabled=False),
            ]
        )
        result = guard.scan(content={"prompt": "test"})
        assert result.passed is True
        assert len(result.results) == 1  # Only enabled scanner ran

    def test_separate_input_output_guards(self, simple_scanner_class):
        # Create separate guards for input and output
        input_guard = TruGuard(type="input")
        output_guard = TruGuard(type="output")

        input_scanner = simple_scanner_class(should_pass=True)
        output_scanner = simple_scanner_class(should_pass=True)

        input_guard.add_scanners([input_scanner])
        output_guard.add_scanners([output_scanner])

        # Scan input - should only use input guard
        result = input_guard.scan(content={"prompt": "test input"})
        assert len(result.results) == 1
        assert result.results[0].scanner_name == "simple_test"

        # Scan output - should only use output guard
        result = output_guard.scan(content={"prompt": "user input", "response": "test output"})
        assert len(result.results) == 1
        assert result.results[0].scanner_name == "simple_test"


class TestTruGuardRepr:
    """Tests for string representation."""

    def test_repr_input_guard(self, guard, simple_scanner_class):
        guard.add_scanners([simple_scanner_class()])
        repr_str = repr(guard)
        assert "TruGuard" in repr_str
        assert "type=input" in repr_str
        assert "scanners=1" in repr_str
        assert "parallel" in repr_str

    def test_repr_output_guard(self, simple_scanner_class):
        output_guard = TruGuard(type="output")
        output_guard.add_scanners([simple_scanner_class()])
        repr_str = repr(output_guard)
        assert "type=output" in repr_str
