"""Tests for _set module."""

from __future__ import annotations

from typing import Any

import pytest

from db_test_helpers.core._errors import UnsupportedError
from db_test_helpers.core._set import set_db_data
from db_test_helpers.core._types import Config


class MockAdapter:
    def __init__(self):
        self.cleared: list[tuple[str, str]] = []
        self.written: list[tuple[str, str, dict[str, Any]]] = []

    def clear_group(self, group: str, parent_path: str) -> None:
        self.cleared.append((group, parent_path))

    def write_record(self, group: str, parent_path: str, data: dict[str, Any]) -> dict[str, Any]:
        self.written.append((group, parent_path, dict(data)))
        return {"__id__": f"generated-{len(self.written)}", **data}

    def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
        return []

    def build_child_path(self, parent_path: str, group: str, record: dict[str, Any]) -> str:
        col_path = f"{parent_path}/{group}" if parent_path else group
        return f"{col_path}/{record['__id__']}"


class TestSetDbDataBasic:
    def test_clears_and_writes(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "Alice"
    age: 30
  - name: "Bob"
    age: 25
"""
        )
        adapter = MockAdapter()
        set_db_data(adapter, str(yaml_file))

        assert adapter.cleared == [("users", "")]
        assert len(adapter.written) == 2
        assert adapter.written[0] == ("users", "", {"name": "Alice", "age": 30})
        assert adapter.written[1] == ("users", "", {"name": "Bob", "age": 25})

    def test_multiple_groups(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "Alice"
posts:
  - title: "Hello"
"""
        )
        adapter = MockAdapter()
        set_db_data(adapter, str(yaml_file))

        assert adapter.cleared == [("users", ""), ("posts", "")]
        assert len(adapter.written) == 2


class TestSetDbDataVarExpansion:
    def test_var_expansion(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "$var(username)"
"""
        )
        adapter = MockAdapter()
        config = Config(variables={"username": "Alice"})
        set_db_data(adapter, str(yaml_file), config)

        assert adapter.written[0][2] == {"name": "Alice"}

    def test_var_in_nested_dict(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      city: "$var(city)"
"""
        )
        adapter = MockAdapter()
        config = Config(variables={"city": "Tokyo"})
        set_db_data(adapter, str(yaml_file), config)

        assert adapter.written[0][2] == {"profile": {"city": "Tokyo"}}

    def test_var_in_list(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - tags:
      - "$var(tag)"
"""
        )
        adapter = MockAdapter()
        config = Config(variables={"tag": "admin"})
        set_db_data(adapter, str(yaml_file), config)

        assert adapter.written[0][2] == {"tags": ["admin"]}


class TestSetDbDataAnyRaises:
    def test_any_raises_value_error(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - id: "$any()"
    name: "Alice"
"""
        )
        adapter = MockAdapter()
        with pytest.raises(ValueError, match="DSL.*not allowed in set"):
            set_db_data(adapter, str(yaml_file))

    def test_any_in_nested_raises(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      id: "$any()"
"""
        )
        adapter = MockAdapter()
        with pytest.raises(ValueError, match="DSL.*not allowed in set"):
            set_db_data(adapter, str(yaml_file))


class TestSetDbDataDslRaises:
    def test_eq_raises_value_error(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$eq(hello)"\n')
        adapter = MockAdapter()
        with pytest.raises(ValueError, match="DSL.*not allowed in set"):
            set_db_data(adapter, str(yaml_file))

    def test_unknown_dsl_raises_value_error(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$unknown(x)"\n')
        adapter = MockAdapter()
        with pytest.raises(ValueError, match="DSL.*not allowed in set"):
            set_db_data(adapter, str(yaml_file))


class TestSetDbDataChildren:
    def test_children_recursive(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "Alice"
    __children__:
      posts:
        - title: "Hello"
"""
        )
        adapter = MockAdapter()
        set_db_data(adapter, str(yaml_file))

        assert adapter.cleared == [("users", ""), ("posts", "users/generated-1")]
        assert adapter.written[0] == ("users", "", {"name": "Alice"})
        assert adapter.written[1] == ("posts", "users/generated-1", {"title": "Hello"})

    def test_build_child_path_receives_actual(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "Alice"
    __children__:
      posts:
        - title: "Hello"
"""
        )

        child_path_records: list[dict] = []

        class TrackingAdapter(MockAdapter):
            def build_child_path(self, parent_path, group, record):
                child_path_records.append(dict(record))
                return super().build_child_path(parent_path, group, record)

        adapter = TrackingAdapter()
        set_db_data(adapter, str(yaml_file))

        assert len(child_path_records) == 1
        assert "__id__" in child_path_records[0]
        assert child_path_records[0]["name"] == "Alice"


class FlatAdapter:
    """Adapter without build_child_path (flat / non-hierarchical)."""

    def __init__(self):
        self.cleared: list[tuple[str, str]] = []
        self.written: list[tuple[str, str, dict[str, Any]]] = []

    def clear_group(self, group: str, parent_path: str) -> None:
        self.cleared.append((group, parent_path))

    def write_record(self, group: str, parent_path: str, data: dict[str, Any]) -> dict[str, Any]:
        self.written.append((group, parent_path, dict(data)))
        return data

    def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
        return []


class TestSetDbDataChildrenUnsupported:
    def test_children_with_flat_adapter_raises(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("""\
users:
  - name: "Alice"
    __children__:
      posts:
        - title: "Hello"
""")
        adapter = FlatAdapter()
        with pytest.raises(UnsupportedError, match="does not support __children__"):
            set_db_data(adapter, str(yaml_file))

    def test_flat_adapter_without_children_works(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("""\
users:
  - name: "Alice"
""")
        adapter = FlatAdapter()
        set_db_data(adapter, str(yaml_file))
        assert adapter.written[0] == ("users", "", {"name": "Alice"})


class TestSetDbDataErrors:
    def test_undefined_variable_raises(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$var(undefined)"\n')
        adapter = MockAdapter()
        with pytest.raises(ValueError, match="Undefined variable"):
            set_db_data(adapter, str(yaml_file))
