"""Tests for composite nesting: cycle detection, depth limits, transitive dependencies."""

import pytest

from ledgrab.storage.color_strip_store import ColorStripStore, MAX_COMPOSITE_DEPTH
from ledgrab.storage.database import Database

# ── Fixtures ──────────────────────────────────────────────────────────


@pytest.fixture
def tmp_db(tmp_path):
    db = Database(tmp_path / "test.db")
    yield db
    db.close()


@pytest.fixture
def store(tmp_db):
    return ColorStripStore(tmp_db)


def _create_static(store: ColorStripStore, name: str) -> str:
    """Create a single-color strip source, return its ID."""
    source = store.create_source(name=name, source_type="single_color", colors=[[255, 0, 0]])
    return source.id


def _create_composite(store: ColorStripStore, name: str, layer_ids: list[str]) -> str:
    """Create a composite source referencing the given layer IDs, return its ID."""
    layers = [
        {"source_id": sid, "blend_mode": "normal", "opacity": 1.0, "enabled": True}
        for sid in layer_ids
    ]
    source = store.create_source(name=name, source_type="composite", layers=layers)
    return source.id


# ── Transitive Dependencies ──────────────────────────────────────────


class TestTransitiveDependencies:
    def test_leaf_has_no_deps(self, store):
        sid = _create_static(store, "leaf")
        assert store.get_transitive_dependencies(sid) == set()

    def test_composite_deps_are_its_layers(self, store):
        a = _create_static(store, "A")
        b = _create_static(store, "B")
        comp = _create_composite(store, "comp", [a, b])
        deps = store.get_transitive_dependencies(comp)
        assert deps == {a, b}

    def test_nested_composite_transitive_deps(self, store):
        a = _create_static(store, "A")
        b = _create_static(store, "B")
        inner = _create_composite(store, "inner", [a])
        outer = _create_composite(store, "outer", [inner, b])
        deps = store.get_transitive_dependencies(outer)
        assert deps == {inner, a, b}

    def test_nonexistent_id_returns_empty(self, store):
        assert store.get_transitive_dependencies("css_nonexist") == set()


# ── Nesting Depth ────────────────────────────────────────────────────


class TestNestingDepth:
    def test_leaf_depth_is_zero(self, store):
        sid = _create_static(store, "leaf")
        assert store.get_nesting_depth(sid) == 0

    def test_flat_composite_depth_is_one(self, store):
        a = _create_static(store, "A")
        comp = _create_composite(store, "comp", [a])
        assert store.get_nesting_depth(comp) == 1

    def test_two_level_nesting_depth(self, store):
        a = _create_static(store, "A")
        inner = _create_composite(store, "inner", [a])
        outer = _create_composite(store, "outer", [inner])
        assert store.get_nesting_depth(outer) == 2

    def test_depth_uses_deepest_branch(self, store):
        a = _create_static(store, "A")
        b = _create_static(store, "B")
        inner = _create_composite(store, "inner", [a])
        outer = _create_composite(store, "outer", [inner, b])
        # inner has depth 1, b has depth 0 → outer picks max (1) + 1 = 2
        assert store.get_nesting_depth(outer) == 2


# ── Validate Nesting ─────────────────────────────────────────────────


class TestValidateNesting:
    def test_self_reference_rejected(self, store):
        a = _create_static(store, "A")
        comp = _create_composite(store, "comp", [a])
        with pytest.raises(ValueError, match="self-reference"):
            store.validate_nesting(comp, [comp])

    def test_direct_cycle_rejected(self, store):
        """A → B → A should be rejected."""
        a = _create_static(store, "A")
        comp_b = _create_composite(store, "B", [a])
        comp_a = _create_composite(store, "A-comp", [comp_b])
        # Try to add A-comp as a layer of B → would create B → A-comp → B
        with pytest.raises(ValueError, match="circular reference"):
            store.validate_nesting(comp_b, [comp_a])

    def test_transitive_cycle_rejected(self, store):
        """A → B → C, then trying C → A should be rejected."""
        leaf = _create_static(store, "leaf")
        c = _create_composite(store, "C", [leaf])
        b = _create_composite(store, "B", [c])
        a = _create_composite(store, "A", [b])
        # Try to add A as a layer of C → C → A → B → C
        with pytest.raises(ValueError, match="circular reference"):
            store.validate_nesting(c, [a])

    def test_valid_nesting_accepted(self, store):
        """A → B where B is composite with only leaves should be fine."""
        leaf = _create_static(store, "leaf")
        inner = _create_composite(store, "inner", [leaf])
        outer_id = _create_composite(store, "outer", [])
        # Should not raise
        store.validate_nesting(outer_id, [inner])

    def test_depth_limit_exceeded(self, store):
        """Build a chain deeper than MAX_COMPOSITE_DEPTH and verify rejection."""
        # Build chain: leaf → comp1 → comp2 → ... → compN
        prev = _create_static(store, "leaf")
        for i in range(MAX_COMPOSITE_DEPTH):
            prev = _create_composite(store, f"comp_{i}", [prev])
        # prev is now at depth MAX_COMPOSITE_DEPTH
        # Trying to wrap it in one more composite should fail
        wrapper = _create_composite(store, "wrapper", [])
        with pytest.raises(ValueError, match="Nesting depth"):
            store.validate_nesting(wrapper, [prev])

    def test_depth_at_limit_accepted(self, store):
        """A chain exactly at MAX_COMPOSITE_DEPTH should be accepted."""
        prev = _create_static(store, "leaf")
        for i in range(MAX_COMPOSITE_DEPTH - 1):
            prev = _create_composite(store, f"comp_{i}", [prev])
        # prev is at depth MAX_COMPOSITE_DEPTH - 1
        wrapper = _create_composite(store, "wrapper", [])
        # 1 + (MAX_COMPOSITE_DEPTH - 1) = MAX_COMPOSITE_DEPTH → should pass
        store.validate_nesting(wrapper, [prev])

    def test_multiple_children_validates_all(self, store):
        """If one child creates a cycle, validation fails even if others are fine."""
        leaf = _create_static(store, "leaf")
        comp = _create_composite(store, "comp", [leaf])
        with pytest.raises(ValueError, match="self-reference"):
            store.validate_nesting(comp, [leaf, comp])

    def test_empty_children_accepted(self, store):
        """Empty children list should always pass."""
        comp = _create_composite(store, "comp", [])
        store.validate_nesting(comp, [])

    def test_none_children_skipped(self, store):
        """Empty-string child IDs should be skipped."""
        comp = _create_composite(store, "comp", [])
        store.validate_nesting(comp, ["", ""])
