"""
Belief Revision System - Test Suite

Comprehensive tests for the generalized BRS framework.
"""
from __future__ import annotations

import tempfile
from pathlib import Path
from typing import List, Tuple

import pytest

# Core imports
from brs.core import (
    Node, Edge, Pattern, WorldBundle, Evidence, TimeRange,
    Maturity, estimate_maturity, canonical_json, content_hash
)
from brs.storage import CASStore
from brs.mesh import (
    PatternSignature, jaccard, WeightedJaccardSimilarity,
    propose_analogs, build_mesh_index
)
from brs.inference import (
    best_path_to_any_root, is_reachable, get_ancestors, get_descendants
)
from brs.revision import (
    make_analog_proposal, shadow_import_analog, evaluate_proposal
)
from brs.discovery import DomainSpec, suggest_analogs, persist_analog_proposals
from brs.domains import registry


# =============================================================================
# Fixtures
# =============================================================================

@pytest.fixture
def temp_store():
    """Create a temporary CASStore for testing."""
    with tempfile.TemporaryDirectory() as tmpdir:
        store = CASStore(Path(tmpdir))
        yield store
        store.close()


@pytest.fixture
def memory_store():
    """Create an in-memory CASStore for fast testing."""
    store = CASStore(Path("/tmp/test"), in_memory=True)
    yield store
    store.close()


@pytest.fixture
def seeded_store(memory_store):
    """Create a store with sample data from two domains."""
    store = memory_store

    # Domain A: Writing-like
    nodes_a = [
        Node(id="A_ROOT", name="Proto-A", node_type="root", properties={}),
        Node(id="A_CHILD1", name="Child1-A", node_type="derived", properties={}),
        Node(id="A_CHILD2", name="Child2-A", node_type="derived", properties={}),
    ]
    edges_a = [
        Edge(id="E_A1", parent_id="A_ROOT", child_id="A_CHILD1",
             relation="direct_descent", tier=0, confidence=0.9),
        Edge(id="E_A2", parent_id="A_ROOT", child_id="A_CHILD2",
             relation="direct_descent", tier=0, confidence=0.85),
    ]
    patterns_a = [
        Pattern(id="PA_1", name="Pattern Alpha",
                summary="Core pattern for domain A with inheritance rules",
                invariants=("All children derive from root",),
                operators=("derive", "inherit")),
    ]

    # Domain B: Biology-like
    nodes_b = [
        Node(id="B_ROOT", name="Ancestor-B", node_type="root", properties={}),
        Node(id="B_CHILD1", name="Species1-B", node_type="species", properties={}),
    ]
    edges_b = [
        Edge(id="E_B1", parent_id="B_ROOT", child_id="B_CHILD1",
             relation="direct_descent", tier=0, confidence=0.95),
    ]
    patterns_b = [
        Pattern(id="PB_1", name="Pattern Beta",
                summary="Core pattern for domain B with descent rules",
                invariants=("Species descend from ancestors",),
                operators=("descend", "inherit")),
    ]

    # Store domain A
    node_ids_a = []
    edge_ids_a = []
    pattern_ids_a = []

    for n in nodes_a:
        h = store.put_object("Node", n)
        store.upsert_node(n.id, n.name, h)
        node_ids_a.append(n.id)

    for e in edges_a:
        h = store.put_object("Edge", e)
        store.upsert_edge(e.id, e.parent_id, e.child_id, e.relation, e.tier, e.confidence, h)
        edge_ids_a.append(e.id)

    for p in patterns_a:
        h = store.put_object("Pattern", p)
        pattern_ids_a.append(p.id)

    world_a = WorldBundle(
        domain_id="domain_a",
        version_label="green",
        node_ids=tuple(node_ids_a),
        edge_ids=tuple(edge_ids_a),
        evidence_ids=(),
        pattern_ids=tuple(pattern_ids_a),
        created_utc="2025-01-01T00:00:00Z"
    )
    store.put_world(world_a)

    # Store domain B
    node_ids_b = []
    edge_ids_b = []
    pattern_ids_b = []

    for n in nodes_b:
        h = store.put_object("Node", n)
        store.upsert_node(n.id, n.name, h)
        node_ids_b.append(n.id)

    for e in edges_b:
        h = store.put_object("Edge", e)
        store.upsert_edge(e.id, e.parent_id, e.child_id, e.relation, e.tier, e.confidence, h)
        edge_ids_b.append(e.id)

    for p in patterns_b:
        h = store.put_object("Pattern", p)
        pattern_ids_b.append(p.id)

    world_b = WorldBundle(
        domain_id="domain_b",
        version_label="green",
        node_ids=tuple(node_ids_b),
        edge_ids=tuple(edge_ids_b),
        evidence_ids=(),
        pattern_ids=tuple(pattern_ids_b),
        created_utc="2025-01-01T00:00:00Z"
    )
    store.put_world(world_b)

    return store


# =============================================================================
# Core Tests
# =============================================================================

class TestCore:
    """Tests for core types and utilities."""

    def test_canonical_json_deterministic(self):
        """canonical_json produces identical output for equivalent dicts."""
        d1 = {"b": 2, "a": 1}
        d2 = {"a": 1, "b": 2}
        assert canonical_json(d1) == canonical_json(d2)

    def test_content_hash_stable(self):
        """content_hash is stable across calls."""
        obj = {"name": "test", "value": 42}
        h1 = content_hash(obj)
        h2 = content_hash(obj)
        assert h1 == h2

    def test_node_creation(self):
        """Node dataclass works correctly."""
        node = Node(
            id="TEST",
            name="Test Node",
            node_type="test",
            properties={"key": "value"},
            first_attested=TimeRange(2000, 2020)
        )
        assert node.id == "TEST"
        assert node.name == "Test Node"
        assert node.first_attested.start_year == 2000

    def test_maturity_scoring(self):
        """Maturity scoring works correctly."""
        m = estimate_maturity(num_nodes=25, num_edges=30, num_tests=10, num_failures=2)
        assert 0 <= m.coverage <= 1
        assert 0 <= m.stability <= 1
        assert m.score > 0


# =============================================================================
# Storage Tests
# =============================================================================

class TestStorage:
    """Tests for CASStore."""

    def test_put_get_object(self, memory_store):
        """Basic put/get round-trip."""
        obj = {"name": "test", "value": 123}
        h = memory_store.put_object("TestObj", obj)
        retrieved = memory_store.get_object(h)
        assert retrieved["kind"] == "TestObj"
        assert retrieved["json"]["name"] == "test"

    def test_object_deduplication(self, memory_store):
        """Identical objects get the same hash."""
        obj = {"name": "test"}
        h1 = memory_store.put_object("Test", obj)
        h2 = memory_store.put_object("Test", obj)
        assert h1 == h2

    def test_world_operations(self, memory_store):
        """World put/get/fork operations."""
        world = WorldBundle(
            domain_id="test",
            version_label="green",
            node_ids=("N1", "N2"),
            edge_ids=(),
            evidence_ids=(),
            pattern_ids=(),
            created_utc="2025-01-01T00:00:00Z"
        )
        memory_store.put_world(world)

        # Get world
        retrieved = memory_store.get_world("test", "green")
        assert retrieved["json"]["domain_id"] == "test"

        # Fork world
        memory_store.fork_world("test", "green", "blue", notes="test fork")
        forked = memory_store.get_world("test", "blue")
        assert forked["json"]["version_label"] == "blue"

    def test_proposal_operations(self, memory_store):
        """Proposal storage and querying."""
        proposal = {"id": "PROP_1", "kind": "test", "data": "value"}
        pid = memory_store.put_proposal("TestProposal", proposal, status="new")

        retrieved = memory_store.get_proposal(pid)
        assert retrieved["status"] == "new"

        memory_store.set_proposal_status(pid, "accepted", notes="approved")
        updated = memory_store.get_proposal(pid)
        assert updated["status"] == "accepted"


# =============================================================================
# Mesh Tests
# =============================================================================

class TestMesh:
    """Tests for mesh and similarity."""

    def test_jaccard_identical(self):
        """Jaccard of identical sets is 1."""
        s = {"a", "b", "c"}
        assert jaccard(s, s) == 1.0

    def test_jaccard_disjoint(self):
        """Jaccard of disjoint sets is 0."""
        assert jaccard({"a", "b"}, {"c", "d"}) == 0.0

    def test_jaccard_partial(self):
        """Jaccard of overlapping sets."""
        j = jaccard({"a", "b", "c"}, {"b", "c", "d"})
        assert 0 < j < 1

    def test_pattern_signature_extraction(self):
        """PatternSignature extracts tokens and operators."""
        pattern = {
            "id": "P1",
            "name": "Test Pattern",
            "summary": "A test with CAPITAL identifiers",
            "invariants": ["Rule one", "Rule two"],
            "operators": ["transform", "derive"]
        }
        sig = PatternSignature.from_pattern("test_domain", pattern)
        assert "test" in sig.tokens
        assert "pattern" in sig.tokens
        assert "transform" in sig.operators
        assert "CAPITAL" in sig.roles

    def test_weighted_similarity(self):
        """WeightedJaccardSimilarity produces scores."""
        sig1 = PatternSignature(
            domain_id="d1", pattern_id="p1", name="Test",
            tokens=frozenset(["a", "b", "c"]),
            roles=frozenset(["X", "Y"]),
            operators=frozenset(["op1"])
        )
        sig2 = PatternSignature(
            domain_id="d2", pattern_id="p2", name="Test2",
            tokens=frozenset(["b", "c", "d"]),
            roles=frozenset(["Y", "Z"]),
            operators=frozenset(["op1", "op2"])
        )
        metric = WeightedJaccardSimilarity()
        score = metric(sig1, sig2)
        assert 0 < score < 1


# =============================================================================
# Inference Tests
# =============================================================================

class TestInference:
    """Tests for graph inference."""

    def test_ancestry_path(self, seeded_store):
        """best_path_to_any_root finds correct paths."""
        result = best_path_to_any_root(seeded_store, "A_CHILD1", ["A_ROOT"])
        assert result is not None
        assert result.root == "A_ROOT"
        assert len(result.steps) >= 1

    def test_no_path_to_unrelated_root(self, seeded_store):
        """No path found to unconnected node."""
        result = best_path_to_any_root(seeded_store, "A_CHILD1", ["B_ROOT"])
        assert result is None

    def test_is_reachable(self, seeded_store):
        """is_reachable correctly identifies connectivity."""
        assert is_reachable(seeded_store, "A_CHILD1", "A_ROOT", direction="up")
        assert not is_reachable(seeded_store, "A_CHILD1", "B_ROOT", direction="up")

    def test_get_ancestors(self, seeded_store):
        """get_ancestors returns correct ancestor set."""
        ancestors = get_ancestors(seeded_store, "A_CHILD1")
        assert "A_ROOT" in ancestors

    def test_get_descendants(self, seeded_store):
        """get_descendants returns correct descendant set."""
        descendants = get_descendants(seeded_store, "A_ROOT")
        assert "A_CHILD1" in descendants
        assert "A_CHILD2" in descendants


# =============================================================================
# Discovery Tests
# =============================================================================

class TestDiscovery:
    """Tests for cross-domain discovery."""

    def test_suggest_analogs(self, seeded_store):
        """suggest_analogs finds cross-domain patterns."""
        domains = [
            DomainSpec("domain_a", "green"),
            DomainSpec("domain_b", "green")
        ]
        suggestions = suggest_analogs(
            seeded_store, domains,
            top_k=10, min_score=0.1
        )
        # Should find some similarity due to shared "inherit" operator
        assert len(suggestions) >= 0  # May or may not find depending on threshold

    def test_persist_analog_proposals(self, seeded_store):
        """persist_analog_proposals creates proposal records."""
        domains = [
            DomainSpec("domain_a", "green"),
            DomainSpec("domain_b", "green")
        ]
        ids = persist_analog_proposals(
            seeded_store, domains,
            top_k=10, min_score=0.1
        )
        # Verify proposals were created
        for pid in ids:
            prop = seeded_store.get_proposal(pid)
            assert prop["kind"] == "AnalogProposal"


# =============================================================================
# Revision Tests
# =============================================================================

class TestRevision:
    """Tests for revision operations."""

    def test_make_analog_proposal(self):
        """make_analog_proposal normalizes and assigns ID."""
        payload = {
            "left_domain": "d1",
            "left_pattern_id": "p1",
            "right_domain": "d2",
            "right_pattern_id": "p2",
            "score": 0.75
        }
        prop = make_analog_proposal(payload)
        assert "id" in prop
        assert prop["kind"] == "AnalogProposal"
        assert prop["score"] == 0.75

    def test_shadow_import(self, seeded_store):
        """shadow_import_analog creates derived patterns."""
        # First create a proposal
        payload = {
            "left_domain": "domain_a",
            "left_pattern_id": "PA_1",
            "left_name": "Pattern Alpha",
            "right_domain": "domain_b",
            "right_pattern_id": "PB_1",
            "right_name": "Pattern Beta",
            "score": 0.7,
            "shared_tokens": ["inherit"],
            "shared_operators": ["inherit"]
        }
        ap = make_analog_proposal(payload)
        pid = seeded_store.put_proposal("AnalogProposal", ap)

        # Shadow import
        h = shadow_import_analog(
            seeded_store,
            proposal_id=pid,
            target_domain="domain_a",
            from_world="green",
            to_world="_shadow_test"
        )
        assert h is not None

        # Verify shadow world exists with new pattern
        shadow = seeded_store.get_world("domain_a", "_shadow_test")["json"]
        assert len(shadow["pattern_ids"]) >= 1


# =============================================================================
# Registry Tests
# =============================================================================

class TestRegistry:
    """Tests for domain registry."""

    def test_register_and_get(self):
        """Registry stores and retrieves smoke tests."""
        registry.clear()

        def test_smoke(store, domain_id, world_label):
            return (1, 0, ["OK"])

        registry.register("test_domain", test_smoke, level="smoke")
        retrieved = registry.get("test_domain", level="smoke")
        assert retrieved is not None

        result = retrieved(None, "test_domain", "green")
        assert result == (1, 0, ["OK"])

    def test_get_levels(self):
        """get_levels returns registered levels."""
        registry.clear()

        def test_fn(store, domain_id, world_label):
            return (0, 0, [])

        registry.register("multi", test_fn, level="smoke")
        registry.register("multi", test_fn, level="regression")

        levels = registry.get_levels("multi")
        assert "smoke" in levels
        assert "regression" in levels

    def test_registered_domains(self):
        """registered_domains returns all domains."""
        registry.clear()

        def test_fn(store, domain_id, world_label):
            return (0, 0, [])

        registry.register("dom1", test_fn)
        registry.register("dom2", test_fn)

        domains = registry.registered_domains()
        assert "dom1" in domains
        assert "dom2" in domains


# =============================================================================
# Integration Tests
# =============================================================================

class TestIntegration:
    """End-to-end integration tests."""

    def test_full_workflow(self, memory_store):
        """Test complete BRS workflow: seed -> discover -> evaluate."""
        # Import example domains
        from brs.examples.writing_domain import seed_writing_domain, DOMAIN_ID as WRITING_ID
        from brs.examples.biology_domain import seed_biology_domain, DOMAIN_ID as BIOLOGY_ID

        # Seed both domains
        seed_writing_domain(memory_store, "green")
        seed_biology_domain(memory_store, "green")

        # Verify worlds exist
        worlds = memory_store.list_worlds()
        domain_ids = [w["domain_id"] for w in worlds]
        assert WRITING_ID in domain_ids
        assert BIOLOGY_ID in domain_ids

        # Discover analogs
        domains = [
            DomainSpec(WRITING_ID, "green"),
            DomainSpec(BIOLOGY_ID, "green")
        ]
        suggestions = suggest_analogs(
            memory_store, domains,
            top_k=5, min_score=0.1
        )

        # There should be some similarity due to shared concepts
        # like "inheritance" in both writing systems and biology
        print(f"Found {len(suggestions)} analog suggestions")

        # If we found any, persist and evaluate
        if suggestions:
            ids = persist_analog_proposals(memory_store, domains, top_k=3, min_score=0.1)
            print(f"Created {len(ids)} proposals")

            # Evaluate first proposal
            if ids:
                result = evaluate_proposal(
                    memory_store,
                    proposal_id=ids[0],
                    target_domain=WRITING_ID,
                    base_world="green",
                    shadow_world="_test_shadow"
                )
                print(f"Evaluation result: {result.outcome}, delta={result.delta:.4f}")


# =============================================================================
# Run Tests
# =============================================================================

if __name__ == "__main__":
    pytest.main([__file__, "-v"])
