"""Tests for _verify module."""

from __future__ import annotations

from typing import Any

import pytest

from db_test_helpers.core._errors import UnsupportedError
from db_test_helpers.core._types import Config
from db_test_helpers.core._verify import MatchError, verify_db_data


class MockAdapter:
    def __init__(self, data: dict[str, list[dict[str, Any]]] | None = None):
        self._data = data or {}

    def clear_group(self, group: str, parent_path: str) -> None:
        pass

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

    def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
        key = f"{parent_path}/{group}" if parent_path else group
        return self._data.get(key, [])

    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.get('__id__', 'unknown')}"


# --- Record count mismatch ---


class TestVerifyRecordCount:
    def test_count_mismatch_raises(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n  - name: Bob\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}]})
        with pytest.raises(AssertionError, match="expected 2.*got 1"):
            verify_db_data(adapter, str(yaml_file))

    def test_count_mismatch_too_many(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}, {"name": "Bob"}]})
        with pytest.raises(AssertionError, match="expected 1.*got 2"):
            verify_db_data(adapter, str(yaml_file))


# --- Strict field comparison ---


class TestVerifyFieldComparison:
    def test_matching_fields(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n    age: 30\n")
        adapter = MockAdapter({"users": [{"name": "Alice", "age": 30}]})
        verify_db_data(adapter, str(yaml_file))

    def test_value_mismatch(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n")
        adapter = MockAdapter({"users": [{"name": "Bob"}]})
        with pytest.raises(AssertionError):
            verify_db_data(adapter, str(yaml_file))

    def test_extra_field_in_actual(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n")
        adapter = MockAdapter({"users": [{"name": "Alice", "extra": "field"}]})
        with pytest.raises(AssertionError, match="extra"):
            verify_db_data(adapter, str(yaml_file))

    def test_missing_field_in_actual(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n    age: 30\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}]})
        with pytest.raises(AssertionError, match="age"):
            verify_db_data(adapter, str(yaml_file))

    def test_type_mismatch(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - age: 30\n')
        adapter = MockAdapter({"users": [{"age": "30"}]})
        with pytest.raises(AssertionError, match="type mismatch"):
            verify_db_data(adapter, str(yaml_file))


# --- DSL matchers ---


class TestVerifyDslMatchers:
    def test_eq_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$eq(Alice)"\n')
        adapter = MockAdapter({"users": [{"name": "Alice"}]})
        verify_db_data(adapter, str(yaml_file))

    def test_lt_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - age: "$lt(100)"\n')
        adapter = MockAdapter({"users": [{"age": 50}]})
        verify_db_data(adapter, str(yaml_file))

    def test_gt_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - age: "$gt(0)"\n')
        adapter = MockAdapter({"users": [{"age": 25}]})
        verify_db_data(adapter, str(yaml_file))

    def test_not_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - role: "$not($eq(admin))"\n')
        adapter = MockAdapter({"users": [{"role": "user"}]})
        verify_db_data(adapter, str(yaml_file))

    def test_and_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - age: "$and($gt(0),$lt(100))"\n')
        adapter = MockAdapter({"users": [{"age": 50}]})
        verify_db_data(adapter, str(yaml_file))

    def test_or_matcher(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - status: "$or($eq(active),$eq(pending))"\n')
        adapter = MockAdapter({"users": [{"status": "active"}]})
        verify_db_data(adapter, str(yaml_file))

    def test_matcher_fail(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - age: "$gt(100)"\n')
        adapter = MockAdapter({"users": [{"age": 50}]})
        with pytest.raises(AssertionError):
            verify_db_data(adapter, str(yaml_file))


# --- $any() wildcard ---


class TestVerifyAnyWildcard:
    def test_any_matches_anything(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$any()"\n')
        adapter = MockAdapter({"users": [{"name": "whatever"}]})
        verify_db_data(adapter, str(yaml_file))

    def test_any_matches_null(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - name: "$any()"\n')
        adapter = MockAdapter({"users": [{"name": None}]})
        verify_db_data(adapter, str(yaml_file))


# --- $var() matching ---


class TestVerifyVar:
    def test_var_matches(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - id: "$var(uid)"\n')
        adapter = MockAdapter({"users": [{"id": "abc-123"}]})
        config = Config(variables={"uid": "abc-123"})
        verify_db_data(adapter, str(yaml_file), config)

    def test_var_mismatch(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - id: "$var(uid)"\n')
        adapter = MockAdapter({"users": [{"id": "different"}]})
        config = Config(variables={"uid": "abc-123"})
        with pytest.raises(AssertionError):
            verify_db_data(adapter, str(yaml_file), config)


# --- Nested dict/list comparison ---


class TestVerifyNestedComparison:
    def test_nested_dict(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      city: Tokyo
      zip: "100-0001"
"""
        )
        adapter = MockAdapter({"users": [{"profile": {"city": "Tokyo", "zip": "100-0001"}}]})
        verify_db_data(adapter, str(yaml_file))

    def test_nested_dict_extra_field(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      city: Tokyo
"""
        )
        adapter = MockAdapter({"users": [{"profile": {"city": "Tokyo", "extra": "x"}}]})
        with pytest.raises(AssertionError, match="extra"):
            verify_db_data(adapter, str(yaml_file))

    def test_list_comparison(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - tags:
      - admin
      - user
"""
        )
        adapter = MockAdapter({"users": [{"tags": ["admin", "user"]}]})
        verify_db_data(adapter, str(yaml_file))

    def test_list_length_mismatch(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - tags:\n      - admin\n")
        adapter = MockAdapter({"users": [{"tags": ["admin", "user"]}]})
        with pytest.raises(AssertionError, match="length"):
            verify_db_data(adapter, str(yaml_file))

    def test_dsl_in_list(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text('users:\n  - tags:\n      - "$any()"\n      - admin\n')
        adapter = MockAdapter({"users": [{"tags": ["whatever", "admin"]}]})
        verify_db_data(adapter, str(yaml_file))


# --- Permutation matching ---


class TestVerifyPermutationMatching:
    def test_two_records_different_order(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n  - name: Bob\n")
        adapter = MockAdapter({"users": [{"name": "Bob"}, {"name": "Alice"}]})
        verify_db_data(adapter, str(yaml_file))

    def test_any_matches_correct_record(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: Alice
    age: 30
  - name: "$any()"
    age: 25
"""
        )
        adapter = MockAdapter(
            {"users": [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]}
        )
        verify_db_data(adapter, str(yaml_file))

    def test_backtracking_required(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - name: "$any()"
    role: admin
  - name: "$any()"
    role: user
"""
        )
        adapter = MockAdapter(
            {"users": [{"name": "Bob", "role": "user"}, {"name": "Alice", "role": "admin"}]}
        )
        verify_db_data(adapter, str(yaml_file))

    def test_no_matching_permutation(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n  - name: Charlie\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}, {"name": "Bob"}]})
        with pytest.raises(AssertionError, match="No matching permutation"):
            verify_db_data(adapter, str(yaml_file))


# --- Children recursive verification ---


class TestVerifyChildren:
    def test_children_verified(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - __id__: "user-1"
    name: Alice
    __children__:
      posts:
        - title: Hello
"""
        )
        adapter = MockAdapter(
            {
                "users": [{"__id__": "user-1", "name": "Alice"}],
                "users/user-1/posts": [{"title": "Hello"}],
            }
        )
        verify_db_data(adapter, str(yaml_file))

    def test_children_mismatch(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - __id__: "user-1"
    name: Alice
    __children__:
      posts:
        - title: Hello
"""
        )
        adapter = MockAdapter(
            {
                "users": [{"__id__": "user-1", "name": "Alice"}],
                "users/user-1/posts": [{"title": "Wrong"}],
            }
        )
        with pytest.raises(AssertionError):
            verify_db_data(adapter, str(yaml_file))


# --- Additional edge cases ---


class TestVerifyEmptyGroup:
    def test_empty_group_both_sides(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users: []\n")
        adapter = MockAdapter({"users": []})
        verify_db_data(adapter, str(yaml_file))

    def test_empty_expected_but_actual_exists(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users: []\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}]})
        with pytest.raises(AssertionError, match="expected 0.*got 1"):
            verify_db_data(adapter, str(yaml_file))


class TestVerifyDslInNestedDict:
    def test_any_in_nested_dict(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      name: "$any()"
      city: "$eq(Tokyo)"
"""
        )
        adapter = MockAdapter({"users": [{"profile": {"name": "whatever", "city": "Tokyo"}}]})
        verify_db_data(adapter, str(yaml_file))

    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({"users": [{"profile": {"city": "Tokyo"}}]})
        config = Config(variables={"city": "Tokyo"})
        verify_db_data(adapter, str(yaml_file), config)


class TestVerifyUnknownDsl:
    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({"users": [{"name": "whatever"}]})
        with pytest.raises(ValueError, match="Unknown DSL function"):
            verify_db_data(adapter, str(yaml_file))

    def test_unknown_dsl_in_nested_dict(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """\
users:
  - profile:
      name: "$foo(bar)"
"""
        )
        adapter = MockAdapter({"users": [{"profile": {"name": "whatever"}}]})
        with pytest.raises(ValueError, match="Unknown DSL function"):
            verify_db_data(adapter, str(yaml_file))


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

    def __init__(self, data: dict[str, list[dict[str, Any]]] | None = None):
        self._data = data or {}

    def clear_group(self, group: str, parent_path: str) -> None:
        pass

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

    def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
        key = f"{parent_path}/{group}" if parent_path else group
        return self._data.get(key, [])


class TestVerifyChildrenUnsupported:
    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({"users": [{"name": "Alice"}]})
        with pytest.raises(UnsupportedError, match="does not support __children__"):
            verify_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({"users": [{"name": "Alice"}]})
        verify_db_data(adapter, str(yaml_file))


class TestVerifyErrorMessages:
    def test_no_matching_permutation_chains_first_error(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n  - name: Charlie\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}, {"name": "Bob"}]})
        with pytest.raises(AssertionError, match="No matching permutation") as exc_info:
            verify_db_data(adapter, str(yaml_file))
        assert isinstance(exc_info.value.__cause__, MatchError)

    def test_extra_field_message_at_root(self, tmp_path):
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n")
        adapter = MockAdapter({"users": [{"name": "Alice", "extra": "x"}]})
        with pytest.raises(AssertionError, match="<root>"):
            verify_db_data(adapter, str(yaml_file))

    def test_type_mismatch_readable_format(self, tmp_path):
        """Type mismatch should show 'expected type X with value V, got type Y with value W'."""
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - age: 30\n")
        adapter = MockAdapter({"users": [{"age": "30"}]})
        with pytest.raises(AssertionError, match=r"expected type int with value 30") as exc_info:
            verify_db_data(adapter, str(yaml_file))
        assert "got type str with value '30'" in str(exc_info.value)

    def test_permutation_failure_shows_expected_and_actual(self, tmp_path):
        """When permutation matching fails with few records, show both expected and actual."""
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n  - name: Charlie\n")
        adapter = MockAdapter({"users": [{"name": "Alice"}, {"name": "Bob"}]})
        with pytest.raises(AssertionError) as exc_info:
            verify_db_data(adapter, str(yaml_file))
        msg = str(exc_info.value)
        assert "Expected records" in msg
        assert "Actual records" in msg
        assert "Charlie" in msg
        assert "Bob" in msg

    def test_permutation_failure_shows_field_mismatches(self, tmp_path):
        """When permutation matching fails, show which fields did not match."""
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text("users:\n  - name: Alice\n    age: 30\n")
        adapter = MockAdapter({"users": [{"name": "Alice", "age": 25}]})
        with pytest.raises(AssertionError, match=r"age"):
            verify_db_data(adapter, str(yaml_file))

    def test_permutation_failure_multiple_records_shows_details(self, tmp_path):
        """With multiple records, error shows per-pair mismatch details."""
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            "users:\n  - name: Alice\n    role: admin\n  - name: Bob\n    role: editor\n"
        )
        adapter = MockAdapter(
            {"users": [{"name": "Alice", "role": "user"}, {"name": "Bob", "role": "viewer"}]}
        )
        with pytest.raises(AssertionError) as exc_info:
            verify_db_data(adapter, str(yaml_file))
        msg = str(exc_info.value)
        # Should mention the field that's wrong
        assert "role" in msg
