"""tests/test_v140_scoping.py — V1.4.0 Feature Tests (#923, #924, #925)"""
from __future__ import annotations
import os, inspect, tempfile, sqlite3
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta
from typing import Optional
import pytest

def _in_past(s=3600):
    return (datetime.now(timezone.utc) - timedelta(seconds=s)).strftime("%Y-%m-%dT%H:%M:%S")

def _in_future(s=3600):
    return (datetime.now(timezone.utc) + timedelta(seconds=s)).strftime("%Y-%m-%dT%H:%M:%S")

@dataclass
class FC:
    id: str; entity_id: Optional[str] = None; entity_type: str = "agent"
    valid_from: Optional[str] = None; valid_to: Optional[str] = None
    created_at: Optional[str] = None

class TestRetrievalConstraints:
    def test_entity_id_field(self):
        from cortex.core.retrieval import RetrievalConstraints
        assert RetrievalConstraints().entity_id is None

    def test_entity_type_field(self):
        from cortex.core.retrieval import RetrievalConstraints
        assert RetrievalConstraints().entity_type is None

    def test_include_expired_defaults_false(self):
        from cortex.core.retrieval import RetrievalConstraints
        assert RetrievalConstraints().include_expired is False

    def test_set_entity_id(self):
        from cortex.core.retrieval import RetrievalConstraints
        c = RetrievalConstraints(entity_id="eva", entity_type="agent")
        assert c.entity_id == "eva" and c.entity_type == "agent"

    def test_set_include_expired(self):
        from cortex.core.retrieval import RetrievalConstraints
        assert RetrievalConstraints(include_expired=True).include_expired is True

class TestEntityScopingFilter:
    def _p(self, c, eid, etype=None):
        if eid is None and etype is None: return True
        if eid is not None and getattr(c, "entity_id", None) != eid: return False
        if etype is not None and getattr(c, "entity_type", "agent") != etype: return False
        return True

    def test_unscoped_returns_all(self):
        for e in ["eva", "kira", None]:
            assert self._p(FC(id="c", entity_id=e), None) is True

    def test_entity_id_filter(self):
        assert self._p(FC(id="c1", entity_id="eva"), "eva") is True
        assert self._p(FC(id="c2", entity_id="kira"), "eva") is False

    def test_entity_type_filter(self):
        assert self._p(FC(id="c1", entity_type="agent"), None, "agent") is True
        assert self._p(FC(id="c2", entity_type="human"), None, "agent") is False

    def test_combined(self):
        c = FC(id="c1", entity_id="eva", entity_type="agent")
        assert self._p(c, "eva", "agent") is True
        assert self._p(c, "eva", "human") is False

class TestEntityMentionCounter:
    def test_increment_exists(self):
        from cortex.storage.queries.entity_queries import EntityQueryMixin
        assert hasattr(EntityQueryMixin, "increment_entity_mentions")

    def test_get_by_mentions_exists(self):
        from cortex.storage.queries.entity_queries import EntityQueryMixin
        assert hasattr(EntityQueryMixin, "get_entities_by_mentions")

    def test_get_by_mentions_signature(self):
        from cortex.storage.queries.entity_queries import EntityQueryMixin
        params = list(inspect.signature(EntityQueryMixin.get_entities_by_mentions).parameters)
        assert "owner_id" in params and "min_mentions" in params

    def test_mentions_wired_in_reconciliation(self):
        from cortex.core.reconciliation import ReconciliationEngine
        assert "increment_entity_mentions" in inspect.getsource(ReconciliationEngine._add_claim)

class TestTemporalValidity:
    def _f(self, ie):
        ns = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
        return lambda c: True if ie else (c.valid_to is None or c.valid_to > ns)

    def test_no_valid_to_passes(self): assert self._f(False)(FC(id="c1"))
    def test_future_passes(self): assert self._f(False)(FC(id="c2", valid_to=_in_future()))
    def test_past_excluded(self): assert not self._f(False)(FC(id="c3", valid_to=_in_past()))
    def test_past_included_flag(self): assert self._f(True)(FC(id="c4", valid_to=_in_past()))
    def test_all_pass_when_expired(self):
        f = self._f(True)
        assert all(f(FC(id=str(i), valid_to=v)) for i,v in enumerate([None,_in_future(),_in_past()]))

class TestContradictionSetsValidTo:
    def test_contradiction_sets_valid_to(self):
        # After #946 split: reconciliation.py → reconciliation/ package.
        # Verify that the supersede path in actions.py still sets valid_to.
        src = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "cortex", "core", "reconciliation", "actions.py")).read()
        assert "valid_to=_now()" in src

    def test_add_claim_valid_from(self):
        from cortex.core.reconciliation import ReconciliationEngine
        assert "valid_from" in inspect.signature(ReconciliationEngine._add_claim).parameters

class TestMemoryTimeline:
    def test_timeline_exists(self):
        from cortex.storage.queries.claim_queries import ClaimQueryMixin
        assert hasattr(ClaimQueryMixin, "get_claims_timeline")

    def test_timeline_all_statuses(self):
        from cortex.storage.queries.claim_queries import ClaimQueryMixin
        assert "status = 'active'" not in inspect.getsource(ClaimQueryMixin.get_claims_timeline)

    def test_timeline_ordered(self):
        claims = [FC(id="c3", valid_from="2025-03"), FC(id="c1", valid_from="2025-01"), FC(id="c2", valid_from="2025-02")]
        assert [c.id for c in sorted(claims, key=lambda x: x.valid_from or "")] == ["c1","c2","c3"]

class TestMigrations:
    def test_v20_attrs(self):
        from cortex.storage.migrations import v20_temporal_scoping as m
        assert m.VERSION == 20 and m.DESCRIPTION

    def test_v20_discovered(self):
        from cortex.storage.migrations.runner import MigrationRunner
        assert 20 in [m.VERSION for m in MigrationRunner("/tmp/_u.db")._load_all_migrations()]

    def test_backfill_present(self):
        from cortex.storage.migrations import v20_temporal_scoping as m
        # Migration uses apply() for idempotency; check backfill is in _POST_SQL
        assert hasattr(m, '_POST_SQL')
        assert "UPDATE semantic_claim SET valid_from = created_at WHERE valid_from IS NULL" in m._POST_SQL

    def test_backfill_sqlite(self):
        dbpath = tempfile.mktemp(suffix=".db")
        try:
            conn = sqlite3.connect(dbpath)
            conn.execute("CREATE TABLE semantic_claim (id TEXT, valid_from TEXT, created_at TEXT)")
            conn.execute("INSERT INTO semantic_claim VALUES ('c1', NULL, '2025-01-01T00:00:00')")
            conn.commit()
            conn.execute("UPDATE semantic_claim SET valid_from = created_at WHERE valid_from IS NULL")
            conn.commit()
            row = conn.execute("SELECT valid_from, created_at FROM semantic_claim WHERE id='c1'").fetchone()
            assert row[0] == row[1]; conn.close()
        finally:
            os.unlink(dbpath)

    def test_indexes_sql(self):
        dbpath = tempfile.mktemp(suffix=".db")
        try:
            conn = sqlite3.connect(dbpath)
            conn.execute("CREATE TABLE semantic_claim (id TEXT, subject TEXT, predicate TEXT, valid_to TEXT, subject_entity_id TEXT, status TEXT)")
            conn.execute("CREATE INDEX IF NOT EXISTS idx_claim_temporal ON semantic_claim(subject, predicate, valid_to)")
            conn.execute("CREATE INDEX IF NOT EXISTS idx_claim_entity_scope ON semantic_claim(subject_entity_id, status)")
            conn.commit(); conn.close()
        finally:
            os.unlink(dbpath)

class TestRequestModels:
    def test_enhanced_retrieve_entity_id(self):
        from cortex.api.models import EnhancedRetrieveRequest
        assert EnhancedRetrieveRequest(query="t", entity_id="eva").entity_id == "eva"

    def test_enhanced_retrieve_include_expired(self):
        from cortex.api.models import EnhancedRetrieveRequest
        assert EnhancedRetrieveRequest(query="t", include_expired=True).include_expired is True

    def test_enhanced_retrieve_defaults(self):
        from cortex.api.models import EnhancedRetrieveRequest
        req = EnhancedRetrieveRequest(query="t")
        assert req.entity_id is None and req.include_expired is False

    def test_retrieve_request_entity_id(self):
        from cortex.api.models import RetrieveRequest
        assert RetrieveRequest(query="t", entity_id="eva").entity_id == "eva"

    def test_memory_search_entity_id(self):
        from cortex.api.models import MemorySearchRequest
        assert MemorySearchRequest(query="t", entity_id="eva").entity_id == "eva"

    def test_timeline_response(self):
        from cortex.api.models import MemoryTimelineResponse
        assert MemoryTimelineResponse(subject="A", items=[], total=0).subject == "A"

    def test_top_entities_response(self):
        from cortex.api.models import TopEntitiesResponse
        assert TopEntitiesResponse(items=[], total=0).total == 0
