"""Tests for the Composition Engine — Merkle Tree + LIVRPS.

Phase 3 Gate:
- LIVRPS resolution correct (strongest arc wins)
- Merkle root changes on edit (O(log n))
- Audit trail is append-only
- Conflict detection works
"""

import hashlib
import json
import os
import tempfile


class TestMerkleTree:
    """Merkle Tree implementation tests (Rule 6)."""

    def test_create_empty_tree(self):
        from harlo.composition.merkle import MerkleTree
        tree = MerkleTree()
        root = tree.get_root()
        assert root is not None
        assert isinstance(root, str)

    def test_create_with_leaves(self):
        from harlo.composition.merkle import MerkleTree
        tree = MerkleTree(["a", "b", "c", "d"])
        root = tree.get_root()
        assert len(root) == 64  # SHA-256 hex digest

    def test_deterministic_root(self):
        from harlo.composition.merkle import MerkleTree
        tree1 = MerkleTree(["a", "b", "c"])
        tree2 = MerkleTree(["a", "b", "c"])
        assert tree1.get_root() == tree2.get_root()

    def test_different_leaves_different_root(self):
        from harlo.composition.merkle import MerkleTree
        tree1 = MerkleTree(["a", "b"])
        tree2 = MerkleTree(["a", "c"])
        assert tree1.get_root() != tree2.get_root()

    def test_update_leaf_changes_root(self):
        """Rule 6: Update must change the root."""
        from harlo.composition.merkle import MerkleTree
        tree = MerkleTree(["a", "b", "c", "d"])
        old_root = tree.get_root()
        tree.update_leaf(0, "new_a")
        new_root = tree.get_root()
        assert old_root != new_root

    def test_update_leaf_is_partial(self):
        """Rule 6: O(log n) partial update, not O(n) full rebuild."""
        from harlo.composition.merkle import MerkleTree
        # We can't directly test O(log n) but we can test it works correctly
        tree = MerkleTree(["a", "b", "c", "d"])
        tree.update_leaf(2, "new_c")
        root = tree.get_root()
        # Rebuild from scratch with the updated leaf
        tree2 = MerkleTree(["a", "b", "new_c", "d"])
        assert root == tree2.get_root()


class TestLIVRPS:
    """LIVRPS resolution tests."""

    def test_local_wins_over_inherit(self):
        """LOCAL (strongest) should win over INHERIT."""
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.resolver import resolve

        stage = MerkleStage("test1")
        stage.add_layer(Layer(
            arc_type=ArcType.INHERIT,
            data={"color": "blue"},
            source="parent",
            timestamp=1000,
            layer_id="l1",
        ))
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"color": "red"},
            source="self",
            timestamp=1001,
            layer_id="l2",
        ))

        resolution = resolve(stage)
        assert resolution.outcome["color"] == "red"

    def test_livrps_priority_order(self):
        """Full LIVRPS priority: LOCAL > INHERIT > VARIANT > REFERENCE > PAYLOAD > SUBLAYER."""
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.resolver import resolve

        stage = MerkleStage("test_priority")
        # Add in reverse priority order
        for i, arc in enumerate([
            ArcType.SUBLAYER, ArcType.PAYLOAD, ArcType.REFERENCE,
            ArcType.VARIANT, ArcType.INHERIT, ArcType.LOCAL,
        ]):
            stage.add_layer(Layer(
                arc_type=arc,
                data={"winner": arc.name},
                source=f"source_{i}",
                timestamp=1000 + i,
                layer_id=f"layer_{i}",
            ))

        resolution = resolve(stage)
        assert resolution.outcome["winner"] == "LOCAL"

    def test_same_arc_type_later_wins(self):
        """When two layers have same arc type, later timestamp wins."""
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.resolver import resolve

        stage = MerkleStage("test_timestamp")
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"value": "first"},
            source="s1",
            timestamp=1000,
            layer_id="l1",
        ))
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"value": "second"},
            source="s2",
            timestamp=2000,
            layer_id="l2",
        ))

        resolution = resolve(stage)
        assert resolution.outcome["value"] == "second"

    def test_non_conflicting_attributes_merged(self):
        """Different attributes from different layers should all appear."""
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.resolver import resolve

        stage = MerkleStage("test_merge")
        stage.add_layer(Layer(
            arc_type=ArcType.INHERIT,
            data={"name": "Alice"},
            source="parent",
            timestamp=1000,
            layer_id="l1",
        ))
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"age": 30},
            source="self",
            timestamp=1001,
            layer_id="l2",
        ))

        resolution = resolve(stage)
        assert resolution.outcome["name"] == "Alice"
        assert resolution.outcome["age"] == 30

    def test_resolution_has_merkle_root(self):
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.resolver import resolve

        stage = MerkleStage("test_root")
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"key": "val"},
            source="s",
            timestamp=1000,
            layer_id="l1",
        ))

        resolution = resolve(stage)
        assert resolution.merkle_root is not None
        assert len(resolution.merkle_root) == 64


class TestConflicts:
    """Conflict detection tests."""

    def test_detect_conflict(self):
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.conflicts import detect_conflicts

        stage = MerkleStage("conflict_test")
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"color": "red"},
            source="s1",
            timestamp=1000,
            layer_id="l1",
        ))
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"color": "blue"},
            source="s2",
            timestamp=1001,
            layer_id="l2",
        ))

        conflicts = detect_conflicts(stage)
        assert len(conflicts) >= 1
        assert conflicts[0].attribute == "color"

    def test_no_conflict_different_attributes(self):
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage
        from harlo.composition.conflicts import detect_conflicts

        stage = MerkleStage("no_conflict")
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"name": "Alice"},
            source="s1",
            timestamp=1000,
            layer_id="l1",
        ))
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"age": 30},
            source="s2",
            timestamp=1001,
            layer_id="l2",
        ))

        conflicts = detect_conflicts(stage)
        assert len(conflicts) == 0


class TestAudit:
    """Audit trail tests."""

    def test_audit_append_only(self):
        """Audit log must be append-only."""
        from pathlib import Path
        from harlo.composition.audit import log_resolution
        from harlo.composition.resolver import Resolution

        # Use a temp file for audit
        import harlo.composition.audit as audit_mod
        fd, temp_path = tempfile.mkstemp(suffix=".log")
        os.close(fd)
        original_path = audit_mod.AUDIT_LOG
        audit_mod.AUDIT_LOG = Path(temp_path)

        try:
            r = Resolution(
                merkle_root="abc123" * 10 + "abcd",
                outcome={"key": "value"},
                trace=["step1"],
            )
            entry_id = log_resolution(r, "stage_1")
            assert entry_id is not None

            # File should have content
            with open(temp_path) as f:
                lines = f.readlines()
            assert len(lines) >= 1

            # Add another entry
            log_resolution(r, "stage_2")
            with open(temp_path) as f:
                lines = f.readlines()
            assert len(lines) >= 2
        finally:
            audit_mod.AUDIT_LOG = original_path
            os.unlink(temp_path)


class TestStage:
    """MerkleStage tests."""

    def test_add_layer_changes_root(self):
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage

        stage = MerkleStage("stage_test")
        root1 = stage.get_merkle_root()
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"key": "val"},
            source="s1",
            timestamp=1000,
            layer_id="l1",
        ))
        root2 = stage.get_merkle_root()
        assert root1 != root2

    def test_stage_serialization(self):
        from harlo.composition.layer import Layer, ArcType
        from harlo.composition.stage import MerkleStage

        stage = MerkleStage("serial_test")
        stage.add_layer(Layer(
            arc_type=ArcType.LOCAL,
            data={"name": "test"},
            source="s1",
            timestamp=1000,
            layer_id="l1",
        ))

        data = stage.to_dict()
        restored = MerkleStage.from_dict(data)
        assert restored.get_merkle_root() == stage.get_merkle_root()
        assert len(restored.get_layers()) == len(stage.get_layers())


class TestCompliance:
    """Phase 3 compliance."""

    def test_no_sleep_in_composition(self):
        import inspect
        from harlo.composition import merkle, layer, stage, resolver, conflicts, audit
        for mod in [merkle, layer, stage, resolver, conflicts, audit]:
            source = inspect.getsource(mod)
            assert "sleep(" not in source, f"{mod.__name__} contains sleep()"

    def test_no_while_true_in_composition(self):
        import inspect
        from harlo.composition import merkle, layer, stage, resolver, conflicts, audit
        for mod in [merkle, layer, stage, resolver, conflicts, audit]:
            source = inspect.getsource(mod)
            assert "while True" not in source, f"{mod.__name__} contains while True"

    def test_no_delete_audit(self):
        """DELETE on audit table = build fail."""
        import inspect
        from harlo.composition import audit
        source = inspect.getsource(audit)
        assert "DELETE" not in source.upper() or "NEVER" in source.upper() or "delete" not in source.lower().replace("never delete", "").replace("# never", ""), \
            "audit.py must not contain DELETE operations"
