"""
Tests for cornerstone system audit fixes (B-CS1 through B-CS5).

Covers:
  - B-CS1: "other" in VALID_CORNERSTONE_CATEGORIES
  - B-CS2: v28 migration SQL_UP is no-op (idempotent via apply())
  - B-CS3: Facade methods route through _store
  - B-CS4: CornerstoneGuardian.DEFAULT_BLOCK_ORDER references injector
  - B-CS5: _resolve_tiktoken_model handles embedding model names
"""

from __future__ import annotations

import hashlib
import pytest
from dataclasses import dataclass
from typing import Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

from cortex.identity.cornerstone import (
    CornerstoneGuardian,
    CornerstoneMemory,
    _count_tokens,
    _resolve_tiktoken_model,
    DEFAULT_TIKTOKEN_MODEL,
)
from cortex.identity.cornerstone_injector import (
    DEFAULT_BLOCK_ORDER as INJECTOR_DEFAULT_BLOCK_ORDER,
)
from cortex.validation import (
    VALID_CORNERSTONE_CATEGORIES,
    validate_cornerstone_category,
)


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


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


class MockStorageAdapter:
    """Minimal mock for testing facade routing."""

    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") -> 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:
        content_hash = sha256(content)
        if cs_id and cs_id in self._store:
            existing = self._store[cs_id]
            existing.content = content
            existing.golden_copy = content
            existing.golden_hash = content_hash
            existing.current_hash = content_hash
            return existing
        cid = self._next_id()
        category = kwargs.get("category", "memory") or "memory"
        cs = CornerstoneMemory(
            id=cid, owner_id=owner_id, label=label, content=content,
            golden_copy=content, golden_hash=content_hash,
            current_hash=content_hash, token_count=0, injection_order=0,
            sealed_by=sealed_by, revision=revision,
            revision_history=revision_history or [], category=category,
        )
        self._store[cid] = cs
        return cs

    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

    async def restore_cornerstone(self, cs_id: str) -> CornerstoneMemory:
        if cs_id not in self._store:
            raise ValueError(f"Cornerstone not found: {cs_id}")
        cs = self._store[cs_id]
        cs.status = "retired"
        return cs

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

    async def retire_cornerstone(self, cs_id: str) -> CornerstoneMemory:
        cs = self._store.get(cs_id)
        if cs is None:
            raise ValueError(f"Cornerstone not found: {cs_id}")
        cs.status = "retired"
        return cs


@dataclass
class MockConfig:
    embedding_model: str = "voyage-code-3"
    max_cornerstones: int = 10
    cornerstone_auto_apply_threshold: Optional[float] = None


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


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


@pytest.fixture
def guardian(storage, config):
    return CornerstoneGuardian(storage=storage, config=config)


# ---------------------------------------------------------------------------
# B-CS1: "other" in VALID_CORNERSTONE_CATEGORIES
# ---------------------------------------------------------------------------


class TestCS1OtherCategory:
    """B-CS1: 'other' must be a valid cornerstone category."""

    def test_other_in_valid_categories(self):
        assert "other" in VALID_CORNERSTONE_CATEGORIES

    def test_validate_other_category(self):
        result = validate_cornerstone_category("other")
        assert result == "other"

    def test_all_expected_categories_present(self):
        expected = {"memory", "anchor", "meta_story", "guardrail", "other"}
        assert expected == VALID_CORNERSTONE_CATEGORIES

    @pytest.mark.asyncio
    async def test_seal_with_other_category(self, guardian):
        cs = await guardian.seal(
            label="Test", content="test content",
            owner_id="test", category="other",
        )
        assert cs.category == "other"

    def test_invalid_category_still_rejected(self):
        with pytest.raises(ValueError, match="Invalid cornerstone category"):
            validate_cornerstone_category("nonexistent")


# ---------------------------------------------------------------------------
# B-CS2: v28 migration SQL_UP is no-op
# ---------------------------------------------------------------------------


class TestCS2MigrationIdempotency:
    """B-CS2: SQL_UP should be a no-op; all logic through apply()."""

    def test_sql_up_is_noop(self):
        from cortex.storage.migrations.v28_cornerstone_category import SQL_UP
        # SQL_UP should be a simple no-op statement
        assert SQL_UP.strip() == "SELECT 1;"

    @pytest.mark.asyncio
    async def test_apply_is_idempotent(self):
        """apply() should handle being called twice (column already exists)."""
        from cortex.storage.migrations.v28_cornerstone_category import apply

        execute_args = []

        mock_cursor = AsyncMock()
        mock_cursor.fetchall = AsyncMock(return_value=[
            (0, "id", "TEXT", 0, None, 0),
            (1, "category", "TEXT", 1, "'memory'", 0),
        ])

        mock_conn = AsyncMock()

        async def mock_execute(sql, *args, **kwargs):
            execute_args.append(sql)
            return mock_cursor

        mock_conn.execute = mock_execute
        mock_conn.commit = AsyncMock()

        # Should not raise even though column exists
        await apply(mock_conn)

        # Should NOT have called ALTER TABLE (column exists)
        alter_calls = [s for s in execute_args if "ALTER TABLE" in s]
        assert len(alter_calls) == 0


# ---------------------------------------------------------------------------
# B-CS3: Facade routes through _store
# ---------------------------------------------------------------------------


class TestCS3StoreRouting:
    """B-CS3: seal/unseal/get_all/verify_integrity must route through _store."""

    @pytest.mark.asyncio
    async def test_seal_routes_through_store(self, guardian):
        """seal() should delegate to _store.create()."""
        guardian._store.create = AsyncMock(return_value=CornerstoneMemory(
            id="cs-mock", owner_id="test", label="Test", content="content",
            golden_copy="content", golden_hash=sha256("content"),
            current_hash=sha256("content"), token_count=2,
        ))
        cs = await guardian.seal(
            label="Test", content="content", owner_id="test",
        )
        guardian._store.create.assert_awaited_once()
        assert cs.id == "cs-mock"

    @pytest.mark.asyncio
    async def test_unseal_routes_through_store(self, guardian):
        """unseal() should delegate to _store.retire()."""
        cs = await guardian.seal(
            label="Test", content="content", owner_id="test",
        )
        guardian._store.retire = AsyncMock(return_value=CornerstoneMemory(
            id=cs.id, owner_id="test", label="Test", content="content",
            golden_copy="content", golden_hash=sha256("content"),
            current_hash=sha256("content"), status="retired",
        ))
        result = await guardian.unseal(cs.id)
        guardian._store.retire.assert_awaited_once_with(cs.id)
        assert result.status == "retired"

    @pytest.mark.asyncio
    async def test_get_all_routes_through_store(self, guardian):
        """get_all() should delegate to _store.get_all()."""
        guardian._store.get_all = AsyncMock(return_value=[])
        result = await guardian.get_all(owner_id="test")
        guardian._store.get_all.assert_awaited_once_with(owner_id="test")
        assert result == []

    @pytest.mark.asyncio
    async def test_verify_integrity_uses_store_for_drift(self, guardian):
        """verify_integrity/check_drift should use _store.update_drift()."""
        cs = await guardian.seal(
            label="Test", content="content", owner_id="test",
        )
        guardian._store.update_drift = AsyncMock()
        reports = await guardian.verify_integrity(owner_id="test")
        assert len(reports) == 1
        assert not reports[0].drifted
        guardian._store.update_drift.assert_awaited()


# ---------------------------------------------------------------------------
# B-CS4: DEFAULT_BLOCK_ORDER references injector (no duplication)
# ---------------------------------------------------------------------------


class TestCS4BlockOrderDedup:
    """B-CS4: CornerstoneGuardian.DEFAULT_BLOCK_ORDER should reference injector's."""

    def test_guardian_block_order_matches_injector(self):
        assert CornerstoneGuardian.DEFAULT_BLOCK_ORDER == INJECTOR_DEFAULT_BLOCK_ORDER

    def test_guardian_block_order_is_same_object(self):
        """Should be the exact same list, not a copy."""
        assert CornerstoneGuardian.DEFAULT_BLOCK_ORDER is INJECTOR_DEFAULT_BLOCK_ORDER

    def test_block_order_includes_other(self):
        assert "other" in CornerstoneGuardian.DEFAULT_BLOCK_ORDER
        assert CornerstoneGuardian.DEFAULT_BLOCK_ORDER[-1] == "other"


# ---------------------------------------------------------------------------
# B-CS5: _resolve_tiktoken_model handles embedding model names
# ---------------------------------------------------------------------------


class TestCS5TiktokenModelResolution:
    """B-CS5: Embedding model names must be mapped to valid tiktoken models."""

    def test_voyage_code_3_mapped(self):
        assert _resolve_tiktoken_model("voyage-code-3") == "cl100k_base"

    def test_voyage_4_mapped(self):
        assert _resolve_tiktoken_model("voyage-4") == "cl100k_base"

    def test_voyage_4_large_mapped(self):
        assert _resolve_tiktoken_model("voyage-4-large") == "cl100k_base"

    def test_openai_embedding_mapped(self):
        assert _resolve_tiktoken_model("text-embedding-3-small") == "cl100k_base"

    def test_gpt4o_passed_through(self):
        """Valid tiktoken model names should pass through unchanged."""
        assert _resolve_tiktoken_model("gpt-4o") == "gpt-4o"

    def test_none_returns_default(self):
        assert _resolve_tiktoken_model(None) == DEFAULT_TIKTOKEN_MODEL

    def test_unknown_model_passed_through(self):
        """Unknown non-embedding models pass through (tiktoken handles KeyError)."""
        assert _resolve_tiktoken_model("some-future-model") == "some-future-model"

    def test_count_tokens_with_embedding_model(self):
        """_count_tokens should work with cl100k_base for embedding models."""
        result = _count_tokens("Hello world", model="cl100k_base")
        assert isinstance(result, int)
        assert result > 0

    def test_guardian_uses_resolved_model(self, config):
        """Guardian should resolve voyage-code-3 to cl100k_base."""
        config.embedding_model = "voyage-code-3"
        storage = MockStorageAdapter()
        g = CornerstoneGuardian(storage=storage, config=config)
        assert g._tiktoken_model == "cl100k_base"

    def test_store_uses_resolved_model(self, config):
        """CornerstoneStore should also resolve embedding model names."""
        from cortex.identity.cornerstone_store import CornerstoneStore
        config.embedding_model = "voyage-code-3"
        storage = MockStorageAdapter()
        store = CornerstoneStore(storage=storage, config=config)
        assert store._tiktoken_model == "cl100k_base"
