"""
Tests for the FGIP Causality Engine.
"""

import json
import sqlite3
import tempfile
from datetime import datetime
from pathlib import Path
import pytest

from helix_cdc.fgip import (
    CausalityEngine,
    EdgeStatus,
    Sensitivity,
    Node,
    Edge,
    Source,
)
from helix_cdc.fgip.schema import init_schema, get_schema_version, SCHEMA_VERSION
from helix_cdc.fgip.queries import (
    trace_causality,
    ownership_loop,
    contradiction_check,
    correction_score,
)
from helix_cdc.fgip.loader import load_seed_nodes, load_seed_edges, validate_seed_file
from helix_cdc.fgip.export import export_graph
from helix_cdc.fgip.sources import hash_file, hash_bytes


class TestSchema:
    """Test SQLite schema initialization."""

    def test_init_schema_creates_tables(self):
        """Schema init creates all required tables."""
        with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
            db_path = Path(f.name)

        try:
            conn = sqlite3.connect(str(db_path))
            receipt = init_schema(conn)

            # Check receipt
            assert receipt['schema_version'] == SCHEMA_VERSION
            assert 'nodes' in receipt['tables_created']
            assert 'edges' in receipt['tables_created']
            assert 'sources' in receipt['tables_created']
            assert 'edge_status_log' in receipt['tables_created']

            # Check tables exist
            cursor = conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
            tables = {row[0] for row in cursor.fetchall()}

            assert 'nodes' in tables
            assert 'edges' in tables
            assert 'sources' in tables
            assert 'edge_sources' in tables
            assert 'claims' in tables

            conn.close()
        finally:
            db_path.unlink()

    def test_get_schema_version(self):
        """Can retrieve schema version after init."""
        with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
            db_path = Path(f.name)

        try:
            conn = sqlite3.connect(str(db_path))
            init_schema(conn)
            version = get_schema_version(conn)
            assert version == SCHEMA_VERSION
            conn.close()
        finally:
            db_path.unlink()


class TestEngine:
    """Test CausalityEngine CRUD operations."""

    @pytest.fixture
    def engine(self):
        """Create a temporary engine for testing."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            db_path = tmpdir / "test.db"
            sources_dir = tmpdir / "sources"
            receipts_dir = tmpdir / "receipts"

            engine = CausalityEngine(db_path, sources_dir, receipts_dir)
            engine.init_db()
            yield engine
            engine.close()

    def test_add_node(self, engine):
        """Can add and retrieve a node."""
        node_id = engine.add_node(
            "ORG:test_org",
            "ORGANIZATION",
            "Test Organization",
        )

        assert node_id == "ORG:test_org"

        node = engine.get_node(node_id)
        assert node is not None
        assert node.name == "Test Organization"
        assert node.node_type == "ORGANIZATION"

    def test_add_edge_claimed_by_default(self, engine):
        """Edges without sources are CLAIMED by default."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("ORG:b", "ORGANIZATION", "Org B")

        edge_id = engine.add_edge(
            "FUNDED",
            "ORG:a",
            "ORG:b",
        )

        edge = engine.get_edge(edge_id)
        assert edge is not None
        assert edge.status == EdgeStatus.CLAIMED

    def test_attach_source_promotes_to_evidenced(self, engine):
        """Attaching a source promotes CLAIMED to EVIDENCED."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("ORG:b", "ORGANIZATION", "Org B")

        edge_id = engine.add_edge("FUNDED", "ORG:a", "ORG:b")

        # Create a test file
        with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f:
            f.write(b"Test evidence content")
            test_file = Path(f.name)

        try:
            receipt = engine.attach_source(edge_id, file=test_file)

            assert receipt['details']['promoted_to_evidenced'] is True

            edge = engine.get_edge(edge_id)
            assert edge.status == EdgeStatus.EVIDENCED
        finally:
            test_file.unlink()

    def test_update_edge_status_logs_change(self, engine):
        """Status updates are logged to edge_status_log."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("ORG:b", "ORGANIZATION", "Org B")

        edge_id = engine.add_edge("FUNDED", "ORG:a", "ORG:b")

        # Update status
        engine.update_edge_status(
            edge_id,
            EdgeStatus.DISPUTED,
            reason="Test dispute",
            actor="test_user",
        )

        # Check log
        cursor = engine._conn.cursor()
        cursor.execute("""
            SELECT * FROM edge_status_log WHERE edge_id = ?
        """, (edge_id,))
        logs = cursor.fetchall()

        assert len(logs) == 1
        assert logs[0][2] == "CLAIMED"  # old_status
        assert logs[0][3] == "DISPUTED"  # new_status

    def test_list_nodes_by_type(self, engine):
        """Can filter nodes by type."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("PERSON:b", "PERSON", "Person B")
        engine.add_node("ORG:c", "ORGANIZATION", "Org C")

        orgs = engine.list_nodes("ORGANIZATION")
        assert len(orgs) == 2

        people = engine.list_nodes("PERSON")
        assert len(people) == 1

    def test_list_edges_by_status(self, engine):
        """Can filter edges by status."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("ORG:b", "ORGANIZATION", "Org B")

        engine.add_edge("FUNDED", "ORG:a", "ORG:b")
        edge2 = engine.add_edge("LOBBIED", "ORG:a", "ORG:b")

        engine.update_edge_status(edge2, EdgeStatus.EVIDENCED, "test", "test")

        claimed = engine.list_edges(status=EdgeStatus.CLAIMED)
        assert len(claimed) == 1

        evidenced = engine.list_edges(status=EdgeStatus.EVIDENCED)
        assert len(evidenced) == 1

    def test_get_stats(self, engine):
        """Can retrieve database statistics."""
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("PERSON:b", "PERSON", "Person B")
        engine.add_edge("EMPLOYED_BY", "PERSON:b", "ORG:a")

        stats = engine.get_stats()

        assert stats['total_nodes'] == 2
        assert stats['total_edges'] == 1
        assert stats['nodes_by_type']['ORGANIZATION'] == 1
        assert stats['nodes_by_type']['PERSON'] == 1


class TestQueries:
    """Test graph query functions."""

    @pytest.fixture
    def populated_db(self):
        """Create a populated test database."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            db_path = tmpdir / "test.db"

            engine = CausalityEngine(db_path, tmpdir / "sources")
            engine.init_db()

            # Create test graph: A -> B -> C -> D
            engine.add_node("ORG:a", "ORGANIZATION", "Org A")
            engine.add_node("ORG:b", "ORGANIZATION", "Org B")
            engine.add_node("ORG:c", "ORGANIZATION", "Org C")
            engine.add_node("ORG:d", "ORGANIZATION", "Org D")

            engine.add_edge("FUNDED", "ORG:a", "ORG:b")
            engine.add_edge("FUNDED", "ORG:b", "ORG:c")
            engine.add_edge("LOBBIED", "ORG:c", "ORG:d")

            yield engine._conn, engine
            engine.close()

    def test_trace_causality_finds_path(self, populated_db):
        """Can trace causal paths between nodes."""
        conn, engine = populated_db

        paths = trace_causality(conn, "ORG:a", "ORG:d")

        assert len(paths) == 1
        path = paths[0]
        assert path.path == ["ORG:a", "ORG:b", "ORG:c", "ORG:d"]
        assert path.total_length == 3

    def test_trace_causality_no_path(self, populated_db):
        """Returns empty list when no path exists."""
        conn, engine = populated_db

        paths = trace_causality(conn, "ORG:d", "ORG:a")  # Reverse direction

        assert len(paths) == 0

    def test_ownership_loop_no_cycle(self, populated_db):
        """Detects absence of ownership cycles."""
        conn, engine = populated_db

        result = ownership_loop(conn, "ORG:a")

        assert result.has_cycle is False

    def test_ownership_loop_with_cycle(self):
        """Detects ownership cycles."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            db_path = tmpdir / "test.db"

            engine = CausalityEngine(db_path, tmpdir / "sources")
            engine.init_db()

            # Create cycle: A -> B -> C -> A
            engine.add_node("ORG:a", "ORGANIZATION", "Org A")
            engine.add_node("ORG:b", "ORGANIZATION", "Org B")
            engine.add_node("ORG:c", "ORGANIZATION", "Org C")

            engine.add_edge("OWNS_SHARES", "ORG:a", "ORG:b")
            engine.add_edge("OWNS_SHARES", "ORG:b", "ORG:c")
            engine.add_edge("OWNS_SHARES", "ORG:c", "ORG:a")

            result = ownership_loop(engine._conn, "ORG:a")

            assert result.has_cycle is True
            assert len(result.cycle_path) == 4  # a -> b -> c -> a
            engine.close()

    def test_correction_score(self, populated_db):
        """Computes correction score for entity."""
        conn, engine = populated_db

        result = correction_score(conn, "ORG:b")

        assert result.entity == "ORG:b"
        assert 0 <= result.score <= 1
        assert result.total_edges >= 1


class TestLoader:
    """Test seed file loading."""

    @pytest.fixture
    def engine(self):
        """Create a temporary engine."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            engine = CausalityEngine(tmpdir / "test.db", tmpdir / "sources")
            engine.init_db()
            yield engine
            engine.close()

    def test_load_seed_nodes(self, engine):
        """Can load nodes from JSON."""
        with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f:
            json.dump([
                {"node_id": "ORG:test1", "node_type": "ORGANIZATION", "name": "Test 1"},
                {"node_id": "ORG:test2", "node_type": "ORGANIZATION", "name": "Test 2"},
            ], f)
            nodes_file = Path(f.name)

        try:
            count, ids = load_seed_nodes(engine, nodes_file)
            assert count == 2
            assert "ORG:test1" in ids
            assert "ORG:test2" in ids
        finally:
            nodes_file.unlink()

    def test_load_seed_edges(self, engine):
        """Can load edges from JSON."""
        # First add nodes
        engine.add_node("ORG:a", "ORGANIZATION", "Org A")
        engine.add_node("ORG:b", "ORGANIZATION", "Org B")

        with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f:
            json.dump([
                {
                    "edge_type": "FUNDED",
                    "from_node_id": "ORG:a",
                    "to_node_id": "ORG:b",
                    "status": "CLAIMED",
                }
            ], f)
            edges_file = Path(f.name)

        try:
            count, ids = load_seed_edges(engine, edges_file, attach_sources=False)
            assert count == 1
        finally:
            edges_file.unlink()

    def test_validate_seed_file_valid(self):
        """Validates correct seed files."""
        with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f:
            json.dump([
                {"node_id": "ORG:test", "node_type": "ORGANIZATION", "name": "Test"},
            ], f)
            path = Path(f.name)

        try:
            is_valid, errors = validate_seed_file(path)
            assert is_valid
            assert len(errors) == 0
        finally:
            path.unlink()

    def test_validate_seed_file_invalid(self):
        """Detects invalid seed files."""
        with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f:
            json.dump([
                {"node_type": "ORGANIZATION"},  # Missing node_id and name
            ], f)
            path = Path(f.name)

        try:
            is_valid, errors = validate_seed_file(path)
            assert not is_valid
            assert len(errors) > 0
        finally:
            path.unlink()


class TestExport:
    """Test graph export."""

    def test_export_json(self):
        """Can export graph to JSON."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            db_path = tmpdir / "test.db"

            engine = CausalityEngine(db_path, tmpdir / "sources")
            engine.init_db()

            engine.add_node("ORG:a", "ORGANIZATION", "Org A")
            engine.add_node("ORG:b", "ORGANIZATION", "Org B")
            engine.add_edge("FUNDED", "ORG:a", "ORG:b")

            output_path = tmpdir / "export.json"
            result = export_graph(engine._conn, output_path)

            assert output_path.exists()
            assert result['statistics']['nodes'] == 2
            assert result['statistics']['edges'] == 1

            engine.close()


class TestSources:
    """Test source artifact management."""

    def test_hash_bytes(self):
        """Can compute SHA256 of bytes."""
        data = b"test content"
        h = hash_bytes(data)

        assert len(h) == 64  # SHA256 hex
        assert h == hash_bytes(data)  # Deterministic

    def test_hash_file(self):
        """Can compute SHA256 of file."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"test content")
            path = Path(f.name)

        try:
            h = hash_file(path)
            assert len(h) == 64
            assert h == hash_bytes(b"test content")
        finally:
            path.unlink()


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