"""
tests/test_contradiction_store.py — Tests for persistent contradiction storage.

Coverage:
  - v11 migration: contradictions table created with correct schema
  - insert_contradiction: basic insert, idempotency, validation
  - find_pair: lookup by (claim_a_id, claim_b_id, owner_id)
  - get_unresolved: filters correctly, ordered oldest-first
  - mark_resolved: updates resolution + resolved_at, validates value
  - list_contradictions: filter by resolved/unresolved, pagination
  - ContradictionTracker.get_unresolved: delegates to DB layer
  - S5ContradictionResolver: auto-resolves by claim status
  - multi-tenant isolation: owner_id scoping

Run: pytest tests/test_contradiction_store.py -v
"""

from __future__ import annotations

import pytest
import pytest_asyncio
from datetime import datetime, timezone

from tests.integration.conftest import _build_plugin


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------


@pytest_asyncio.fixture
async def storage(tmp_path):
    """A fully-initialised (all migrations applied) SQLiteAdapter, closed after each test."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    db_path = str(tmp_path / "test_contradictions.db")
    adapter = SQLiteAdapter(db_path=db_path, enable_wal=False)
    await adapter.initialize()
    yield adapter
    await adapter.close()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _iso(ts: str) -> datetime:
    """Parse an ISO-8601 string into a timezone-aware datetime."""
    return datetime.fromisoformat(ts)


async def _insert(storage, *, claim_a="c-new", claim_b="c-old", owner="test", **kw) -> int:
    """Convenience wrapper around insert_contradiction."""
    from cortex.core.contradiction_store import insert_contradiction

    return await insert_contradiction(
        storage,
        claim_a_id=claim_a,
        claim_b_id=claim_b,
        owner_id=owner,
        **kw,
    )


# ===========================================================================
# Migration — schema
# ===========================================================================


class TestMigration:
    async def test_contradictions_table_exists(self, storage):
        """v11 migration should have created the contradictions table."""
        conn = await storage._get_conn()
        async with conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='contradictions'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None, "contradictions table not found"

    async def test_contradictions_columns(self, storage):
        """All required columns should be present."""
        conn = await storage._get_conn()
        async with conn.execute("PRAGMA table_info(contradictions)") as cur:
            rows = await cur.fetchall()
        cols = {r["name"] for r in rows}
        required = {"id", "claim_a_id", "claim_b_id", "detected_at", "resolution",
                    "resolved_at", "owner_id", "reason"}
        assert required <= cols, f"Missing columns: {required - cols}"

    async def test_schema_version_is_11(self, storage):
        """Migration runner should have bumped schema_version to at least 11."""
        conn = await storage._get_conn()
        async with conn.execute(
            "SELECT value FROM cortex_config WHERE key = 'schema_version'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert int(row["value"]) >= 11


# ===========================================================================
# insert_contradiction
# ===========================================================================


class TestInsertContradiction:
    async def test_basic_insert_returns_int(self, storage):
        row_id = await _insert(storage)
        assert isinstance(row_id, int)
        assert row_id > 0

    async def test_idempotent_same_pair_same_owner(self, storage):
        id1 = await _insert(storage, claim_a="a", claim_b="b", owner="o1")
        id2 = await _insert(storage, claim_a="a", claim_b="b", owner="o1")
        assert id1 == id2

    async def test_different_pair_different_id(self, storage):
        id1 = await _insert(storage, claim_a="a", claim_b="b")
        id2 = await _insert(storage, claim_a="a", claim_b="c")
        assert id1 != id2

    async def test_same_pair_different_owner_not_idempotent(self, storage):
        """Same pair but different owner → two separate rows."""
        id1 = await _insert(storage, claim_a="a", claim_b="b", owner="owner1")
        id2 = await _insert(storage, claim_a="a", claim_b="b", owner="owner2")
        assert id1 != id2

    async def test_stores_reason(self, storage):
        from cortex.core.contradiction_store import find_pair

        await _insert(storage, claim_a="a", claim_b="b", reason="temporal mismatch")
        row = await find_pair(storage, "a", "b", "test")
        assert row is not None
        assert row["reason"] == "temporal mismatch"

    async def test_stores_resolution(self, storage):
        from cortex.core.contradiction_store import find_pair

        await _insert(storage, claim_a="a", claim_b="b", resolution="kept_a")
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolution"] == "kept_a"

    async def test_null_resolution_by_default(self, storage):
        from cortex.core.contradiction_store import find_pair

        await _insert(storage, claim_a="a", claim_b="b")
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolution"] is None

    async def test_invalid_resolution_raises(self, storage):
        from cortex.core.contradiction_store import insert_contradiction

        with pytest.raises(ValueError, match="Invalid resolution"):
            await insert_contradiction(
                storage, "a", "b", "test", resolution="bad_value"
            )

    async def test_detected_at_defaults_to_now(self, storage):
        from cortex.core.contradiction_store import find_pair

        before = datetime.now(timezone.utc)
        await _insert(storage, claim_a="a", claim_b="b")
        row = await find_pair(storage, "a", "b", "test")
        detected = _iso(row["detected_at"])
        assert detected >= before

    async def test_custom_detected_at_stored(self, storage):
        from cortex.core.contradiction_store import find_pair

        ts = "2025-01-01T00:00:00+00:00"
        await _insert(storage, claim_a="a", claim_b="b", detected_at=ts)
        row = await find_pair(storage, "a", "b", "test")
        assert row["detected_at"] == ts


# ===========================================================================
# find_pair
# ===========================================================================


class TestFindPair:
    async def test_returns_none_when_not_found(self, storage):
        from cortex.core.contradiction_store import find_pair

        result = await find_pair(storage, "x", "y", "owner")
        assert result is None

    async def test_returns_dict_with_all_fields(self, storage):
        from cortex.core.contradiction_store import find_pair

        await _insert(storage, claim_a="a", claim_b="b")
        row = await find_pair(storage, "a", "b", "test")
        assert row is not None
        for key in ("id", "claim_a_id", "claim_b_id", "detected_at",
                    "resolution", "resolved_at", "owner_id", "reason"):
            assert key in row

    async def test_scoped_to_owner_id(self, storage):
        from cortex.core.contradiction_store import find_pair

        await _insert(storage, claim_a="a", claim_b="b", owner="owner1")
        result = await find_pair(storage, "a", "b", "owner2")
        assert result is None


# ===========================================================================
# get_unresolved
# ===========================================================================


class TestGetUnresolved:
    async def test_empty_returns_empty_list(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        result = await get_unresolved(storage, "nobody")
        assert result == []

    async def test_unresolved_rows_returned(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        await _insert(storage, claim_a="c", claim_b="d", owner="o")
        rows = await get_unresolved(storage, "o")
        assert len(rows) == 2

    async def test_resolved_rows_excluded(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        await _insert(storage, claim_a="a", claim_b="b", owner="o", resolution="kept_a")
        rows = await get_unresolved(storage, "o")
        assert rows == []

    async def test_mixed_only_unresolved_returned(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        await _insert(storage, claim_a="a", claim_b="b", owner="o")                    # unresolved
        await _insert(storage, claim_a="c", claim_b="d", owner="o", resolution="kept_b")  # resolved
        rows = await get_unresolved(storage, "o")
        assert len(rows) == 1
        assert rows[0]["claim_a_id"] == "a"

    async def test_scoped_to_owner(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        await _insert(storage, claim_a="a", claim_b="b", owner="o1")
        await _insert(storage, claim_a="c", claim_b="d", owner="o2")
        rows = await get_unresolved(storage, "o1")
        assert len(rows) == 1
        assert rows[0]["owner_id"] == "o1"

    async def test_ordered_oldest_first(self, storage):
        from cortex.core.contradiction_store import get_unresolved

        await _insert(storage, claim_a="a", claim_b="b", owner="o",
                      detected_at="2025-01-01T00:00:00+00:00")
        await _insert(storage, claim_a="c", claim_b="d", owner="o",
                      detected_at="2025-01-02T00:00:00+00:00")
        rows = await get_unresolved(storage, "o")
        assert rows[0]["claim_a_id"] == "a"
        assert rows[1]["claim_a_id"] == "c"


# ===========================================================================
# mark_resolved
# ===========================================================================


class TestMarkResolved:
    async def test_mark_resolved_updates_row(self, storage):
        from cortex.core.contradiction_store import find_pair, mark_resolved

        row_id = await _insert(storage, claim_a="a", claim_b="b")
        ok = await mark_resolved(storage, row_id, "kept_a")
        assert ok is True
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolution"] == "kept_a"

    async def test_mark_resolved_sets_resolved_at(self, storage):
        from cortex.core.contradiction_store import find_pair, mark_resolved

        before = datetime.now(timezone.utc)
        row_id = await _insert(storage, claim_a="a", claim_b="b")
        await mark_resolved(storage, row_id, "kept_b")
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolved_at"] is not None
        resolved_dt = _iso(row["resolved_at"])
        assert resolved_dt >= before

    async def test_mark_resolved_kept_b(self, storage):
        from cortex.core.contradiction_store import find_pair, mark_resolved

        row_id = await _insert(storage, claim_a="a", claim_b="b")
        await mark_resolved(storage, row_id, "kept_b")
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolution"] == "kept_b"

    async def test_mark_resolved_merged(self, storage):
        from cortex.core.contradiction_store import find_pair, mark_resolved

        row_id = await _insert(storage, claim_a="a", claim_b="b")
        await mark_resolved(storage, row_id, "merged")
        row = await find_pair(storage, "a", "b", "test")
        assert row["resolution"] == "merged"

    async def test_mark_resolved_missing_id_returns_false(self, storage):
        from cortex.core.contradiction_store import mark_resolved

        ok = await mark_resolved(storage, 99999, "kept_a")
        assert ok is False

    async def test_mark_resolved_invalid_raises(self, storage):
        from cortex.core.contradiction_store import mark_resolved

        row_id = await _insert(storage, claim_a="a", claim_b="b")
        with pytest.raises(ValueError, match="Invalid resolution"):
            await mark_resolved(storage, row_id, "bad")

    async def test_mark_resolved_removes_from_unresolved(self, storage):
        from cortex.core.contradiction_store import get_unresolved, mark_resolved

        row_id = await _insert(storage, claim_a="a", claim_b="b", owner="o")
        rows = await get_unresolved(storage, "o")
        assert len(rows) == 1

        await mark_resolved(storage, row_id, "kept_a")
        rows = await get_unresolved(storage, "o")
        assert rows == []


# ===========================================================================
# list_contradictions
# ===========================================================================


class TestListContradictions:
    async def test_list_all(self, storage):
        from cortex.core.contradiction_store import list_contradictions

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        await _insert(storage, claim_a="c", claim_b="d", owner="o", resolution="kept_a")
        rows = await list_contradictions(storage, "o")
        assert len(rows) == 2

    async def test_filter_resolved_true(self, storage):
        from cortex.core.contradiction_store import list_contradictions

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        await _insert(storage, claim_a="c", claim_b="d", owner="o", resolution="kept_a")
        rows = await list_contradictions(storage, "o", resolved=True)
        assert len(rows) == 1
        assert rows[0]["resolution"] == "kept_a"

    async def test_filter_resolved_false(self, storage):
        from cortex.core.contradiction_store import list_contradictions

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        await _insert(storage, claim_a="c", claim_b="d", owner="o", resolution="kept_a")
        rows = await list_contradictions(storage, "o", resolved=False)
        assert len(rows) == 1
        assert rows[0]["resolution"] is None

    async def test_limit(self, storage):
        from cortex.core.contradiction_store import list_contradictions

        for i in range(5):
            await _insert(storage, claim_a=f"a{i}", claim_b=f"b{i}", owner="o")
        rows = await list_contradictions(storage, "o", limit=3)
        assert len(rows) == 3

    async def test_offset(self, storage):
        from cortex.core.contradiction_store import list_contradictions

        for i in range(5):
            await _insert(
                storage,
                claim_a=f"a{i}",
                claim_b=f"b{i}",
                owner="o",
                detected_at=f"2025-01-0{i+1}T00:00:00+00:00",
            )
        all_rows = await list_contradictions(storage, "o", limit=100)
        paginated = await list_contradictions(storage, "o", limit=2, offset=2)
        assert len(paginated) == 2
        # Results are DESC by detected_at; offset skips the 2 newest
        assert paginated[0]["claim_a_id"] not in {all_rows[0]["claim_a_id"], all_rows[1]["claim_a_id"]}


# ===========================================================================
# ContradictionTracker.get_unresolved (DB delegation)
# ===========================================================================


class TestTrackerGetUnresolved:
    async def test_get_unresolved_delegates_to_db(self, storage):
        from cortex.core.contradictions import ContradictionTracker

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        tracker = ContradictionTracker()
        rows = await tracker.get_unresolved(storage, "o")
        assert len(rows) == 1
        assert rows[0]["claim_a_id"] == "a"

    async def test_get_unresolved_empty(self, storage):
        from cortex.core.contradictions import ContradictionTracker

        tracker = ContradictionTracker()
        rows = await tracker.get_unresolved(storage, "nobody")
        assert rows == []

    async def test_get_from_db_all(self, storage):
        from cortex.core.contradictions import ContradictionTracker

        await _insert(storage, claim_a="a", claim_b="b", owner="o")
        await _insert(storage, claim_a="c", claim_b="d", owner="o", resolution="kept_a")
        tracker = ContradictionTracker()
        all_rows = await tracker.get_from_db(storage, "o", resolved=None)
        assert len(all_rows) == 2


# ===========================================================================
# S5ContradictionResolver — dream-cycle stage
# ===========================================================================


async def _create_claim(storage, claim_id: str, status: str = "active",
                        owner_id: str = "test") -> None:
    """Insert a minimal semantic_claim row for S5 testing."""
    async with storage._write_lock:
        conn = await storage._get_conn()
        await conn.execute(
            """
            INSERT OR IGNORE INTO semantic_claim
                (id, owner_id, subject, predicate, object_value, status,
                 confidence, created_at, updated_at)
            VALUES (?, ?, 'subj', 'pred', 'val', ?, 0.9,
                    datetime('now'), datetime('now'))
            """,
            (claim_id, owner_id, status),
        )
        await conn.commit()


class TestS5ContradictionResolver:
    async def test_no_unresolved_returns_zero_resolved(self, storage):
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")
        assert result.resolved_count == 0
        assert result.total_unresolved == 0
        assert result.success is True

    async def test_resolves_kept_a_when_b_superseded(self, storage):
        """claim_a active + claim_b superseded → kept_a."""
        from cortex.core.contradiction_store import find_pair
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "ca", status="active")
        await _create_claim(storage, "cb", status="superseded")
        row_id = await _insert(storage, claim_a="ca", claim_b="cb", owner="o")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")

        assert result.resolved_count == 1
        row = await find_pair(storage, "ca", "cb", "o")
        assert row["resolution"] == "kept_a"
        assert row["resolved_at"] is not None

    async def test_resolves_kept_b_when_a_superseded(self, storage):
        """claim_a superseded + claim_b active → kept_b (edge case)."""
        from cortex.core.contradiction_store import find_pair
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "ca", status="superseded")
        await _create_claim(storage, "cb", status="active")
        await _insert(storage, claim_a="ca", claim_b="cb", owner="o")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")

        assert result.resolved_count == 1
        row = await find_pair(storage, "ca", "cb", "o")
        assert row["resolution"] == "kept_b"

    async def test_resolves_merged_when_both_superseded(self, storage):
        """Both superseded → merged."""
        from cortex.core.contradiction_store import find_pair
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "ca", status="superseded")
        await _create_claim(storage, "cb", status="superseded")
        await _insert(storage, claim_a="ca", claim_b="cb", owner="o")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")

        assert result.resolved_count == 1
        row = await find_pair(storage, "ca", "cb", "o")
        assert row["resolution"] == "merged"

    async def test_skips_when_claims_still_active(self, storage):
        """Both claims active → cannot decide, skip."""
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "ca", status="active")
        await _create_claim(storage, "cb", status="active")
        await _insert(storage, claim_a="ca", claim_b="cb", owner="o")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")

        assert result.resolved_count == 0
        assert result.skipped_count == 1

    async def test_resolves_merged_when_both_missing(self, storage):
        """Both claims missing (no row in semantic_claim) → merged."""
        from cortex.core.contradiction_store import find_pair
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _insert(storage, claim_a="gone1", claim_b="gone2", owner="o")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o")

        assert result.resolved_count == 1
        row = await find_pair(storage, "gone1", "gone2", "o")
        assert row["resolution"] == "merged"

    async def test_multi_tenant_isolation(self, storage):
        """S5 run for owner1 should not touch owner2's contradictions."""
        from cortex.core.contradiction_store import get_unresolved
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "ca", status="active", owner_id="o1")
        await _create_claim(storage, "cb", status="superseded", owner_id="o1")
        await _create_claim(storage, "cx", status="active", owner_id="o2")
        await _create_claim(storage, "cy", status="superseded", owner_id="o2")

        await _insert(storage, claim_a="ca", claim_b="cb", owner="o1")
        await _insert(storage, claim_a="cx", claim_b="cy", owner="o2")

        resolver = S5ContradictionResolver(storage)
        result = await resolver.run("o1")

        # o1 resolved
        assert result.resolved_count == 1
        # o2 still unresolved
        o2_rows = await get_unresolved(storage, "o2")
        assert len(o2_rows) == 1
        assert o2_rows[0]["resolution"] is None


# ===========================================================================
# Integration: full record → persist → S5 resolve flow
# ===========================================================================


class TestEndToEnd:
    async def test_insert_and_resolve_via_s5(self, storage):
        """End-to-end: insert a contradiction, then S5 resolves it."""
        from cortex.core.contradiction_store import find_pair
        from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver

        await _create_claim(storage, "winner", status="active")
        await _create_claim(storage, "loser", status="superseded")
        await _insert(storage, claim_a="winner", claim_b="loser", owner="default")

        resolver = S5ContradictionResolver(storage)
        s5 = await resolver.run("default")

        assert s5.resolved_count == 1
        row = await find_pair(storage, "winner", "loser", "default")
        assert row["resolution"] == "kept_a"
        assert row["resolved_at"] is not None

    async def test_tracker_sees_db_written_by_store(self, storage):
        """ContradictionTracker.get_unresolved() sees rows written directly to DB."""
        from cortex.core.contradiction_store import insert_contradiction
        from cortex.core.contradictions import ContradictionTracker

        await insert_contradiction(
            storage, "c-new", "c-old", "myowner", reason="test reason"
        )
        tracker = ContradictionTracker()
        # In-memory is empty — tracker has no records
        assert len(tracker) == 0

        # But DB query via tracker picks up the row
        rows = await tracker.get_unresolved(storage, "myowner")
        assert len(rows) == 1
        assert rows[0]["reason"] == "test reason"
