# SPDX-License-Identifier: Apache-2.0
"""Tests for claim extraction and evidence/conflict operations."""

import pytest

from nlai import Claim, Evidence, extract_claims, assertiveness_score, attach_evidence, contest_claim
from nlai.claims import (
    STRENGTH_ASSERTIVE, STRENGTH_HEDGED,
    STATUS_UNSUPPORTED, STATUS_SUPPORTED, STATUS_CONTESTED,
)


class TestClaim:
    """Claim dataclass behavior."""

    def test_frozen(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        with pytest.raises(AttributeError):
            c.text = "other"  # type: ignore[misc]

    def test_invalid_strength(self):
        with pytest.raises(ValueError, match="Invalid strength"):
            Claim(text="test", strength="invalid")

    def test_defaults(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        assert c.status == STATUS_UNSUPPORTED
        assert c.span == (0, 0)

    def test_roundtrip(self):
        c = Claim(text="definitely", strength=STRENGTH_ASSERTIVE,
                  span=(5, 15))
        d = c.to_dict()
        c2 = Claim.from_dict(d)
        assert c == c2


class TestExtractClaims:
    """Assertive and hedged claim extraction."""

    def test_no_claims(self):
        claims = extract_claims("The weather is nice today.")
        assert claims == []

    def test_assertive_claim(self):
        claims = extract_claims("This is definitely the right approach.")
        assert len(claims) >= 1
        assertive = [c for c in claims if c.strength == STRENGTH_ASSERTIVE]
        assert len(assertive) >= 1
        assert any("definitely" in c.text.lower() for c in assertive)

    def test_hedged_claim(self):
        claims = extract_claims("I think this might work.")
        hedged = [c for c in claims if c.strength == STRENGTH_HEDGED]
        assert len(hedged) >= 1

    def test_multiple_claims(self):
        """Multiple keywords in separate sentences produce multiple claims."""
        text = "I think it works. It's definitely correct."
        claims = extract_claims(text)
        assert len(claims) >= 2

    def test_founded_in(self):
        claims = extract_claims("Apple was founded in 1976.")
        assertive = [c for c in claims if c.strength == STRENGTH_ASSERTIVE]
        assert len(assertive) >= 1

    def test_is_located(self):
        claims = extract_claims("The office is located in New York.")
        assertive = [c for c in claims if c.strength == STRENGTH_ASSERTIVE]
        assert len(assertive) >= 1

    def test_span_tracking(self):
        text = "It is definitely true."
        claims = extract_claims(text)
        assert len(claims) >= 1
        c = claims[0]
        assert text[c.span[0]:c.span[1]] == c.text

    def test_no_duplicates(self):
        """Same sentence should not produce duplicate claims."""
        text = "definitely definitely"
        claims = extract_claims(text)
        spans = [c.span for c in claims]
        assert len(spans) == len(set(spans))

    def test_sentence_level_text(self):
        """Claims should contain the surrounding sentence, not just the keyword."""
        text = "The system is definitely fast."
        claims = extract_claims(text)
        assert len(claims) == 1
        assert "fast" in claims[0].text
        assert "definitely" in claims[0].text

    def test_multiple_sentences(self):
        """Multiple sentences produce separate claims."""
        text = "It is definitely fast. It is certainly reliable."
        claims = extract_claims(text)
        assert len(claims) == 2

    def test_mixed_strength_same_sentence(self):
        """Assertive + hedged in same sentence → assertive wins."""
        text = "I think it definitely works."
        claims = extract_claims(text)
        assert len(claims) == 1
        assert claims[0].strength == "assertive"

    def test_sentence_boundary_newlines(self):
        """Newlines are sentence boundaries."""
        text = "It is definitely true\nI think it works"
        claims = extract_claims(text)
        assert len(claims) == 2


class TestAssertivenessScore:
    """Assertiveness scoring."""

    def test_no_claims_zero(self):
        assert assertiveness_score("Hello world.") == 0.0

    def test_assertive_text(self):
        text = ("This is definitely correct, clearly right, "
                "obviously true, certainly valid, undoubtedly so.")
        score = assertiveness_score(text)
        assert score == 1.0

    def test_hedged_lower(self):
        """Hedged claims contribute less than assertive."""
        hedged_score = assertiveness_score("I think it works.")
        assertive_score = assertiveness_score("It definitely works.")
        assert assertive_score > hedged_score

    def test_range(self):
        """Score is always in [0.0, 1.0]."""
        for text in ["", "hello", "definitely", "I think maybe"]:
            score = assertiveness_score(text)
            assert 0.0 <= score <= 1.0


class TestAttachEvidence:
    """Evidence attachment and status transitions."""

    def test_unsupported_to_supported(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e = Evidence(kind="test_result", reference="exit 0")
        c2 = attach_evidence(c, e)
        assert c2.status == STATUS_SUPPORTED
        assert e in c2.evidence

    def test_returns_new_instance(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e = Evidence(kind="receipt", reference="abc")
        c2 = attach_evidence(c, e)
        assert c is not c2
        assert c.status == STATUS_UNSUPPORTED  # original unchanged

    def test_contested_stays_contested(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE, status=STATUS_CONTESTED)
        e = Evidence(kind="receipt", reference="abc")
        c2 = attach_evidence(c, e)
        assert c2.status == STATUS_CONTESTED
        assert e in c2.evidence

    def test_duplicate_evidence_idempotent(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e = Evidence(kind="receipt", reference="abc")
        c2 = attach_evidence(c, e)
        c3 = attach_evidence(c2, e)
        assert c2 is c3  # same object returned
        assert len(c3.evidence) == 1

    def test_multiple_evidence(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e1 = Evidence(kind="test_result", reference="exit 0")
        e2 = Evidence(kind="citation", reference="https://example.com")
        c2 = attach_evidence(attach_evidence(c, e1), e2)
        assert len(c2.evidence) == 2

    def test_evidence_remains_after_contest(self):
        """Evidence survives contest (compound state)."""
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e = Evidence(kind="receipt", reference="abc")
        c2 = attach_evidence(c, e)
        c3 = contest_claim(c2, "conflicting text")
        assert c3.status == STATUS_CONTESTED
        assert e in c3.evidence

    def test_attach_contest_attach_still_contested(self):
        """attach → contest → attach: status remains CONTESTED."""
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        e1 = Evidence(kind="receipt", reference="abc")
        e2 = Evidence(kind="test_result", reference="exit 0")
        c2 = attach_evidence(c, e1)
        c3 = contest_claim(c2, "conflict")
        c4 = attach_evidence(c3, e2)
        assert c4.status == STATUS_CONTESTED
        assert len(c4.evidence) == 2


class TestContestClaim:
    """Claim contest behavior."""

    def test_sets_contested(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        c2 = contest_claim(c, "opposing claim")
        assert c2.status == STATUS_CONTESTED
        assert "opposing claim" in c2.conflicts_with

    def test_supported_to_contested(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE, status=STATUS_SUPPORTED)
        c2 = contest_claim(c, "opposing claim")
        assert c2.status == STATUS_CONTESTED

    def test_duplicate_conflict_idempotent(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        c2 = contest_claim(c, "opposing claim")
        c3 = contest_claim(c2, "opposing claim")
        assert c2 is c3
        assert len(c3.conflicts_with) == 1

    def test_empty_conflicting_text_rejected(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE)
        with pytest.raises(ValueError, match="non-empty"):
            contest_claim(c, "")


class TestClaimEvidenceSerialization:
    """Claim serialization with evidence and conflicts."""

    def test_claim_with_evidence_roundtrip(self):
        e = Evidence(kind="receipt", reference="abc")
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE,
                  status=STATUS_SUPPORTED, evidence=(e,))
        d = c.to_dict()
        c2 = Claim.from_dict(d)
        assert c == c2

    def test_claim_with_conflicts_roundtrip(self):
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE,
                  status=STATUS_CONTESTED, conflicts_with=("opposing",))
        d = c.to_dict()
        c2 = Claim.from_dict(d)
        assert c == c2

    def test_backward_compat_missing_evidence(self):
        """from_dict without evidence/conflicts_with fields (0.1.0 compat)."""
        d = {"text": "test", "strength": "assertive", "status": "unsupported", "span": [0, 0]}
        c = Claim.from_dict(d)
        assert c.evidence == ()
        assert c.conflicts_with == ()

    def test_evidence_sorted_canonically(self):
        """Evidence sorted by (kind, reference, source) in to_dict output."""
        e1 = Evidence(kind="receipt", reference="zzz")
        e2 = Evidence(kind="citation", reference="aaa")
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE,
                  status=STATUS_SUPPORTED, evidence=(e1, e2))
        d = c.to_dict()
        # citation comes before receipt alphabetically
        assert d["evidence"][0]["kind"] == "citation"
        assert d["evidence"][1]["kind"] == "receipt"

    def test_conflicts_sorted_canonically(self):
        """conflicts_with sorted alphabetically in to_dict output."""
        c = Claim(text="test", strength=STRENGTH_ASSERTIVE,
                  status=STATUS_CONTESTED, conflicts_with=("zebra", "alpha"))
        d = c.to_dict()
        assert d["conflicts_with"] == ["alpha", "zebra"]
