"""
Unit tests for CornerstoneStore — pure storage layer (issue #711).

Tests are isolated: all storage operations use a MockStorageAdapter (no SQLite).
These tests verify the store's CRUD contract independently of the injection layer.
"""

from __future__ import annotations

import hashlib
import pytest
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional

from cortex.identity.cornerstone import (
    CornerstoneMemory,
    DEFAULT_MAX_CORNERSTONES,
    _sha256,
)
from cortex.identity.cornerstone_store import CornerstoneStore


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

def sha256(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).digest().hex()


def make_cs(
    cs_id: str,
    label: str,
    content: str,
    owner_id: str = "default",
    status: str = "active",
    injection_order: int = 0,
    category: Optional[str] = "memory",
    revision: int = 1,
) -> CornerstoneMemory:
    h = sha256(content)
    return CornerstoneMemory(
        id=cs_id,
        owner_id=owner_id,
        label=label,
        content=content,
        golden_copy=content,
        golden_hash=h,
        current_hash=h,
        status=status,
        injection_order=injection_order,
        category=category,
        revision=revision,
        token_count=len(content.split()),
    )


# ---------------------------------------------------------------------------
# Mock storage adapter
# ---------------------------------------------------------------------------

class MockStorageAdapter:
    def __init__(self):
        self._store: Dict[str, CornerstoneMemory] = {}
        self._counter = 0

    def _next_id(self) -> str:
        self._counter += 1
        return f"cs-{self._counter:04d}"

    async def get_cornerstones(
        self, owner_id: str = "default", status: str = "active"
    ) -> List[CornerstoneMemory]:
        if owner_id == "":
            return list(self._store.values())
        return [c for c in self._store.values() if c.owner_id == owner_id]

    async def seal_cornerstone(
        self,
        label: str,
        content: str,
        owner_id: str = "default",
        sealed_by: str = "operator",
        cs_id: Optional[str] = None,
        revision: int = 1,
        revision_history: Optional[list] = None,
        **kwargs,
    ) -> CornerstoneMemory:
        h = sha256(content)
        if cs_id is not None and cs_id in self._store:
            existing = self._store[cs_id]
            existing.content = content
            existing.golden_copy = content
            existing.golden_hash = h
            existing.current_hash = h
            existing.revision = revision
            existing.revision_history = revision_history or existing.revision_history
            existing.sealed_by = sealed_by
            return existing
        cid = self._next_id()
        category = kwargs.get("category", "memory")
        cs = CornerstoneMemory(
            id=cid,
            owner_id=owner_id,
            label=label,
            content=content,
            golden_copy=content,
            golden_hash=h,
            current_hash=h,
            sealed_by=sealed_by,
            status="active",
            revision=revision,
            revision_history=revision_history or [],
            category=category,
            token_count=len(content.split()),
        )
        self._store[cid] = cs
        return cs

    async def retire_cornerstone(self, cs_id: str) -> CornerstoneMemory:
        cs = self._store[cs_id]
        cs.status = "retired"
        return cs

    async def get_cornerstone_by_id(
        self, cs_id: str, owner_id: Optional[str] = None
    ) -> Optional[CornerstoneMemory]:
        return self._store.get(cs_id)

    async def update_cornerstone_drift(
        self, cs_id: str, drift_score: float
    ) -> None:
        if cs_id in self._store:
            self._store[cs_id].drift_score = drift_score


@dataclass
class MockConfig:
    max_cornerstones: int = DEFAULT_MAX_CORNERSTONES
    embedding_model: str = "gpt-4o"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def storage():
    return MockStorageAdapter()


@pytest.fixture
def config():
    return MockConfig()


@pytest.fixture
def store(storage, config):
    return CornerstoneStore(storage=storage, config=config)


# ---------------------------------------------------------------------------
# Tests: create
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_create_basic(store):
    cs = await store.create(label="I am Eva", content="Eva is the AI.")
    assert cs.id is not None
    assert cs.label == "I am Eva"
    assert cs.content == "Eva is the AI."
    assert cs.status == "active"
    assert cs.golden_copy == "Eva is the AI."
    assert cs.golden_hash == sha256("Eva is the AI.")
    assert cs.current_hash == cs.golden_hash


@pytest.mark.asyncio
async def test_create_sets_injection_order(store):
    cs1 = await store.create(label="First", content="First cornerstone.")
    cs2 = await store.create(label="Second", content="Second cornerstone.")
    assert cs1.injection_order == 0
    assert cs2.injection_order == 1


@pytest.mark.asyncio
async def test_create_enforces_cap(storage):
    """Cap is enforced at the configured max. Use a fresh store with cap=2."""
    cfg = MockConfig(max_cornerstones=2)
    capped_store = CornerstoneStore(storage=storage, config=cfg)
    await capped_store.create(label="A", content="Alpha")
    await capped_store.create(label="B", content="Beta")
    with pytest.raises(ValueError, match="cap reached"):
        await capped_store.create(label="C", content="Gamma")


@pytest.mark.asyncio
async def test_create_category_passed(store, storage):
    cs = await store.create(label="Guardrail", content="I refuse.", category="guardrail")
    # The storage was called with category=guardrail
    stored = list(storage._store.values())[0]
    assert stored.category == "guardrail"


# ---------------------------------------------------------------------------
# Tests: get_all
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_all_returns_only_active(store):
    cs1 = await store.create(label="Active", content="Active CS.")
    cs_retired = await store.create(label="Retired", content="Retired CS.")
    await store.retire(cs_retired.id)
    active = await store.get_all()
    assert len(active) == 1
    assert active[0].id == cs1.id


@pytest.mark.asyncio
async def test_get_all_sorted_by_injection_order(store):
    cs1 = await store.create(label="First", content="A")
    cs2 = await store.create(label="Second", content="B")
    result = await store.get_all()
    assert result[0].id == cs1.id
    assert result[1].id == cs2.id


@pytest.mark.asyncio
async def test_get_all_owner_scoping(store):
    await store.create(label="Eva", content="Eva CS.", owner_id="eva")
    await store.create(label="Kira", content="Kira CS.", owner_id="kira")
    eva_cs = await store.get_all(owner_id="eva")
    kira_cs = await store.get_all(owner_id="kira")
    assert len(eva_cs) == 1
    assert eva_cs[0].label == "Eva"
    assert len(kira_cs) == 1
    assert kira_cs[0].label == "Kira"


# ---------------------------------------------------------------------------
# Tests: get_by_id
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_by_id_found(store):
    cs = await store.create(label="Test", content="Content.")
    result = await store.get_by_id(cs.id)
    assert result is not None
    assert result.id == cs.id


@pytest.mark.asyncio
async def test_get_by_id_not_found(store):
    result = await store.get_by_id("nonexistent-id")
    assert result is None


# ---------------------------------------------------------------------------
# Tests: retire
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_retire_sets_status(store):
    cs = await store.create(label="Retiring", content="Goodbye.")
    retired = await store.retire(cs.id)
    assert retired.status == "retired"


@pytest.mark.asyncio
async def test_retire_not_found(store):
    with pytest.raises(ValueError, match="not found"):
        await store.retire("nonexistent-id")


@pytest.mark.asyncio
async def test_retire_already_retired(store):
    cs = await store.create(label="Retiring", content="Goodbye.")
    await store.retire(cs.id)
    with pytest.raises(ValueError, match="already retired"):
        await store.retire(cs.id)


# ---------------------------------------------------------------------------
# Tests: update_content
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_update_content_updates_fields(store):
    cs = await store.create(label="Evolving", content="Original text.")
    revised = await store.update_content(
        cs_id=cs.id,
        new_content="Revised text.",
        reason="Testing revision.",
    )
    assert revised.content == "Revised text."
    assert revised.golden_copy == "Revised text."
    assert revised.golden_hash == sha256("Revised text.")
    assert revised.revision == 2


@pytest.mark.asyncio
async def test_update_content_not_found(store):
    with pytest.raises(ValueError, match="not found"):
        await store.update_content("bad-id", "New.", "reason")


@pytest.mark.asyncio
async def test_update_content_retired_raises(store):
    cs = await store.create(label="Old", content="Old content.")
    await store.retire(cs.id)
    with pytest.raises(ValueError, match="retired"):
        await store.update_content(cs.id, "New content.", "reason")


# ---------------------------------------------------------------------------
# Tests: update_drift
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_update_drift_records_score(store, storage):
    cs = await store.create(label="Drift test", content="Content.")
    await store.update_drift(cs.id, drift_score=1.0)
    assert storage._store[cs.id].drift_score == 1.0


# ---------------------------------------------------------------------------
# Tests: get_total_token_count
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_total_token_count(store):
    await store.create(label="A", content="one two three")      # 3 tokens
    await store.create(label="B", content="four five six seven")  # 4 tokens
    total = await store.get_total_token_count()
    assert total == 7


@pytest.mark.asyncio
async def test_get_total_token_count_empty(store):
    total = await store.get_total_token_count()
    assert total == 0


# ---------------------------------------------------------------------------
# Tests: helpers
# ---------------------------------------------------------------------------

def test_compute_hash(store):
    text = "Eva is the AI."
    assert store.compute_hash(text) == sha256(text)


def test_count_tokens_nonzero(store):
    count = store.count_tokens("Hello world, this is a test sentence.")
    assert count > 0


def test_check_cap_ok(storage):
    cfg = MockConfig(max_cornerstones=3)
    s = CornerstoneStore(storage=storage, config=cfg)
    # Should not raise when under cap
    s.check_cap(2)


def test_check_cap_exceeded(storage):
    cfg = MockConfig(max_cornerstones=3)
    s = CornerstoneStore(storage=storage, config=cfg)
    with pytest.raises(ValueError, match="cap reached"):
        s.check_cap(3)
