"""Tests for sanitizer module.

Tests written from contracts + RFC spec ONLY.
Must FAIL until implementation is complete.
"""

import pytest

from lattice.core.types.evidence import SecretSpan
from lattice.core.sanitizer import detect_secrets, sanitize, _merge_overlapping_spans


class TestDetectSecrets:
    """Tests for detect_secrets function."""

    def test_empty_content_returns_empty_list(self) -> None:
        """Empty content should return empty list of spans."""
        result = detect_secrets("", [])
        assert result == []

    def test_no_patterns_returns_empty_list(self) -> None:
        """No patterns means no detection."""
        result = detect_secrets("some content", [])
        assert result == []

    def test_no_match_returns_empty_list(self) -> None:
        """Pattern not found returns empty list."""
        result = detect_secrets("hello world", [r"api_key"])
        assert result == []

    def test_detects_api_key(self) -> None:
        """Detect API key pattern in content."""
        content = "api_key=sk-abc123def456"
        patterns = [r"sk-[a-zA-Z0-9]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 1
        assert result[0].start == 8
        assert result[0].end == 23  # "sk-abc123def456" is 15 chars, ends at 8+15=23
        assert result[0].pattern == "sk-[a-zA-Z0-9]+"

    def test_detects_bearer_token(self) -> None:
        """Detect Bearer token pattern."""
        content = "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9"
        patterns = [r"Bearer\s+[a-zA-Z0-9_\-\.]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 1
        assert result[0].start == 15

    def test_detects_sk_prefix_key(self) -> None:
        """Detect OpenAI-style sk- prefix keys."""
        content = "OPENAI_API_KEY=sk-proj-abc123xyz789"
        patterns = [r"sk-[a-zA-Z0-9\-]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 1
        assert content[result[0].start : result[0].end].startswith("sk-")

    def test_detects_multiple_secrets(self) -> None:
        """Detect multiple secrets in same content."""
        content = "key1=sk-aaa key2=sk-bbb key3=sk-ccc"
        patterns = [r"sk-[a-z]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 3

    def test_detects_secrets_with_different_patterns(self) -> None:
        """Multiple patterns can detect different secret types."""
        content = "api_key=sk-abc password=secret123"
        patterns = [r"sk-[a-z]+", r"password=[a-zA-Z0-9]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 2

    def test_overlapping_spans_allowed(self) -> None:
        """detect_secrets may return overlapping spans."""
        content = "sk-abc123"
        # Two patterns that overlap
        patterns = [r"sk-[a-z]+", r"sk-[a-z0-9]+"]
        result = detect_secrets(content, patterns)
        # Both should match, may overlap
        assert len(result) >= 1

    def test_returns_secret_span_objects(self) -> None:
        """Result is list of SecretSpan objects."""
        content = "key=sk-abc"
        patterns = [r"sk-[a-z]+"]
        result = detect_secrets(content, patterns)
        assert len(result) == 1
        assert isinstance(result[0], SecretSpan)
        assert hasattr(result[0], "start")
        assert hasattr(result[0], "end")
        assert hasattr(result[0], "pattern")

    def test_span_positions_valid(self) -> None:
        """All spans have start >= 0 and end > start."""
        content = "token=sk-xyz789"
        patterns = [r"sk-[a-z0-9]+"]
        result = detect_secrets(content, patterns)
        for span in result:
            assert span.start >= 0
            assert span.end > span.start
            # Verify span actually matches in content
            assert content[span.start : span.end] == "sk-xyz789"


class TestSanitize:
    """Tests for sanitize function."""

    def test_empty_content_returns_empty(self) -> None:
        """Empty content returns empty string."""
        result = sanitize("", [])
        assert result == ""

    def test_no_secrets_returns_unchanged(self) -> None:
        """Content without secrets passes unchanged."""
        content = "Hello, world! This is safe content."
        result = sanitize(content, [])
        assert result == content

    def test_no_patterns_returns_unchanged(self) -> None:
        """No patterns means no sanitization."""
        content = "api_key=sk-secret123"
        result = sanitize(content, [])
        assert result == content

    def test_replaces_single_secret(self) -> None:
        """Single secret is replaced with [REDACTED]."""
        content = "api_key=sk-abc123"
        patterns = [r"sk-[a-z0-9]+"]
        result = sanitize(content, patterns)
        assert result == "api_key=[REDACTED]"

    def test_uses_custom_replacement(self) -> None:
        """Custom replacement string is used."""
        content = "key=sk-secret"
        patterns = [r"sk-[a-z]+"]
        result = sanitize(content, patterns, replacement="***HIDDEN***")
        assert result == "key=***HIDDEN***"

    def test_replaces_multiple_secrets(self) -> None:
        """Multiple secrets are all replaced."""
        content = "key1=sk-aaa key2=sk-bbb"
        patterns = [r"sk-[a-z]+"]
        result = sanitize(content, patterns)
        assert result == "key1=[REDACTED] key2=[REDACTED]"

    def test_different_secret_types(self) -> None:
        """Different patterns sanitize different secret types."""
        content = "api_key=sk-abc token=xyz789"
        patterns = [r"sk-[a-z]+", r"xyz[0-9]+"]
        result = sanitize(content, patterns)
        assert "sk-abc" not in result
        assert "xyz789" not in result
        assert "[REDACTED]" in result

    def test_output_length_leq_input_length(self) -> None:
        """Output length should be <= input length."""
        content = "key=sk-abcdefghij"
        patterns = [r"sk-[a-z]+"]
        result = sanitize(content, patterns)
        assert len(result) <= len(content)

    def test_merges_overlapping_spans(self) -> None:
        """Overlapping secrets are merged before replacement."""
        content = "sk-abc123"
        # Patterns that would overlap
        patterns = [r"sk-abc", r"sk-abc123"]
        result = sanitize(content, patterns)
        # Should only have one redaction, not embedded redactions
        assert result.count("[REDACTED]") == 1

    def test_preserves_non_secret_content(self) -> None:
        """Non-secret content is preserved."""
        content = "user: alice, api_key=sk-secret, role: admin"
        patterns = [r"sk-[a-z]+"]
        result = sanitize(content, patterns)
        assert "user: alice" in result
        assert "role: admin" in result
        assert "sk-secret" not in result


class TestMergeOverlappingSpans:
    """Tests for _merge_overlapping_spans function."""

    def test_empty_list_returns_empty(self) -> None:
        """Empty list returns empty list."""
        result = _merge_overlapping_spans([])
        assert result == []

    def test_single_span_unchanged(self) -> None:
        """Single span is returned unchanged."""
        spans = [SecretSpan(0, 10, "p1")]
        result = _merge_overlapping_spans(spans)
        assert len(result) == 1
        assert result[0].start == 0
        assert result[0].end == 10

    def test_non_overlapping_spans_unchanged(self) -> None:
        """Non-overlapping spans are returned as-is."""
        spans = [
            SecretSpan(0, 5, "p1"),
            SecretSpan(10, 15, "p2"),
        ]
        result = _merge_overlapping_spans(spans)
        assert len(result) == 2

    def test_merges_overlapping_spans(self) -> None:
        """Overlapping spans are merged into one."""
        spans = [
            SecretSpan(0, 10, "p1"),
            SecretSpan(5, 15, "p2"),
        ]
        result = _merge_overlapping_spans(spans)
        assert len(result) == 1
        assert result[0].start == 0
        assert result[0].end == 15

    def test_merges_adjacent_spans(self) -> None:
        """Adjacent spans (touching) are merged."""
        spans = [
            SecretSpan(0, 5, "p1"),
            SecretSpan(5, 10, "p2"),
        ]
        result = _merge_overlapping_spans(spans)
        assert len(result) == 1
        assert result[0].start == 0
        assert result[0].end == 10

    def test_multiple_overlapping_groups(self) -> None:
        """Multiple groups of overlapping spans are merged separately."""
        spans = [
            SecretSpan(0, 10, "p1"),
            SecretSpan(5, 15, "p2"),
            SecretSpan(20, 30, "p3"),
            SecretSpan(25, 35, "p4"),
        ]
        result = _merge_overlapping_spans(spans)
        assert len(result) == 2
        # First merged group
        assert result[0].start == 0
        assert result[0].end == 15
        # Second merged group
        assert result[1].start == 20
        assert result[1].end == 35

    def test_result_sorted_by_start(self) -> None:
        """Result is sorted by start position."""
        spans = [
            SecretSpan(20, 30, "p3"),
            SecretSpan(0, 10, "p1"),
            SecretSpan(10, 20, "p2"),
        ]
        result = _merge_overlapping_spans(spans)
        for i in range(len(result) - 1):
            assert result[i].start < result[i + 1].start

    def test_returns_secret_span_objects(self) -> None:
        """Result contains SecretSpan objects."""
        spans = [
            SecretSpan(0, 10, "p1"),
            SecretSpan(5, 15, "p2"),
        ]
        result = _merge_overlapping_spans(spans)
        for span in result:
            assert isinstance(span, SecretSpan)


class TestContractValidation:
    """Tests that validate @pre/@post contracts are enforced."""

    def test_detect_secrets_rejects_invalid_content_type(self) -> None:
        """detect_secrets should reject non-string content."""
        with pytest.raises(Exception):  # deal.PreContractError
            detect_secrets(123, [r"pattern"])  # type: ignore

    def test_detect_secrets_rejects_invalid_patterns_type(self) -> None:
        """detect_secrets should reject non-list patterns."""
        with pytest.raises(Exception):  # deal.PreContractError
            detect_secrets("content", "not a list")  # type: ignore

    def test_detect_secrets_rejects_non_string_patterns(self) -> None:
        """detect_secrets should reject patterns with non-string items."""
        with pytest.raises(Exception):  # deal.PreContractError
            detect_secrets("content", [123, r"valid"])  # type: ignore

    def test_sanitize_rejects_invalid_replacement_type(self) -> None:
        """sanitize should reject non-string replacement."""
        with pytest.raises(Exception):  # deal.PreContractError
            sanitize("content", [r"pattern"], replacement=123)  # type: ignore

    def test_merge_spans_rejects_non_list(self) -> None:
        """_merge_overlapping_spans should reject non-list input."""
        with pytest.raises(Exception):  # deal.PreContractError
            _merge_overlapping_spans("not a list")  # type: ignore

    def test_merge_spans_rejects_invalid_span_order(self) -> None:
        """_merge_overlapping_spans should reject spans with start > end."""
        invalid_spans = [SecretSpan(10, 5, "invalid")]  # start > end
        with pytest.raises(Exception):  # deal.PreContractError
            _merge_overlapping_spans(invalid_spans)
