# SPDX-License-Identifier: Apache-2.0
"""Tests for anchor constraints and lexical matching."""

import pytest

from nlai import Anchor, Violation, check_text
from nlai.anchors import (
    SEVERITY_WARN, SEVERITY_CORRECT, SEVERITY_REJECT,
    CONSTRAINT_INVARIANT, CONSTRAINT_PREFERENCE,
)


class TestAnchor:
    """Anchor dataclass behavior."""

    def test_frozen(self):
        a = Anchor(id="test", description="Test anchor")
        with pytest.raises(AttributeError):
            a.id = "other"  # type: ignore[misc]

    def test_defaults(self):
        a = Anchor(id="test", description="Test")
        assert a.required == ()
        assert a.forbidden == ()
        assert a.severity == SEVERITY_CORRECT
        assert a.constraint_class == CONSTRAINT_PREFERENCE

    def test_invalid_severity(self):
        with pytest.raises(ValueError, match="Invalid severity"):
            Anchor(id="test", description="Test", severity="invalid")

    def test_invalid_constraint_class(self):
        with pytest.raises(ValueError, match="Invalid constraint_class"):
            Anchor(id="test", description="Test", constraint_class="invalid")

    def test_roundtrip(self):
        a = Anchor(
            id="test", description="Test",
            required=("foo",), forbidden=("bar",),
            severity=SEVERITY_REJECT,
            constraint_class=CONSTRAINT_INVARIANT,
        )
        d = a.to_dict()
        a2 = Anchor.from_dict(d)
        assert a == a2

    def test_from_dict_defaults(self):
        d = {"id": "test", "description": "Test"}
        a = Anchor.from_dict(d)
        assert a.severity == SEVERITY_CORRECT
        assert a.constraint_class == CONSTRAINT_PREFERENCE


class TestViolation:
    """Violation dataclass behavior."""

    def test_frozen(self):
        v = Violation(anchor_id="a", severity="warn", description="bad")
        with pytest.raises(AttributeError):
            v.description = "other"  # type: ignore[misc]

    def test_roundtrip(self):
        v = Violation(
            anchor_id="test", severity="reject",
            description="Found bad thing",
            evidence=("matched 'foo'",),
        )
        d = v.to_dict()
        v2 = Violation.from_dict(d)
        assert v == v2


class TestCheckText:
    """Lexical matching against anchors."""

    def test_no_anchors(self):
        violations = check_text("hello world", [])
        assert violations == []

    def test_no_violations(self):
        a = Anchor(id="test", description="Test", required=("hello",))
        violations = check_text("hello world", [a])
        assert violations == []

    def test_required_missing(self):
        a = Anchor(id="test", description="Test", required=("goodbye",))
        violations = check_text("hello world", [a])
        assert len(violations) == 1
        assert violations[0].anchor_id == "test"
        assert "Required pattern missing" in violations[0].description

    def test_forbidden_present(self):
        a = Anchor(id="test", description="Test", forbidden=("world",))
        violations = check_text("hello world", [a])
        assert len(violations) == 1
        assert "Forbidden pattern found" in violations[0].description

    def test_case_insensitive(self):
        a = Anchor(id="test", description="Test", forbidden=("HELLO",))
        violations = check_text("hello world", [a])
        assert len(violations) == 1  # Case-insensitive match

    def test_multiple_anchors(self):
        anchors = [
            Anchor(id="a1", description="A1", required=("missing",)),
            Anchor(id="a2", description="A2", forbidden=("hello",)),
        ]
        violations = check_text("hello world", anchors)
        assert len(violations) == 2

    def test_multiple_forbidden(self):
        a = Anchor(id="test", description="Test",
                   forbidden=("hello", "world"))
        violations = check_text("hello world", [a])
        assert len(violations) == 2

    def test_severity_propagated(self):
        a = Anchor(id="test", description="Test",
                   forbidden=("bad",), severity=SEVERITY_REJECT)
        violations = check_text("this is bad", [a])
        assert violations[0].severity == SEVERITY_REJECT

    def test_no_match_no_violation(self):
        a = Anchor(id="test", description="Test",
                   forbidden=("xyz",), required=("hello",))
        violations = check_text("hello world", [a])
        assert violations == []

    def test_regex_special_chars_escaped(self):
        """Patterns with regex metacharacters are escaped."""
        a = Anchor(id="test", description="Test",
                   forbidden=("foo.bar",))
        # "foo.bar" as literal, not regex
        violations = check_text("fooxbar", [a])
        assert violations == []  # "." should not match "x"
        violations = check_text("foo.bar", [a])
        assert len(violations) == 1
