"""
Tests for the Topological Dependency Resolver.
"""

import pytest

from atomingest.core.dependency_resolver import (
    DependencyResolver,
    CircularDependencyError,
    resolve_ingestion_order,
)


class TestDependencyResolver:
    """Tests for DependencyResolver class."""

    @pytest.fixture
    def resolver(self):
        """Create a fresh resolver instance for each test."""
        return DependencyResolver()

    def test_resolver_initialization(self, resolver):
        """Test that resolver initializes with empty state."""
        assert resolver.dependency_graph == {}
        assert resolver.table_to_config == {}

    def test_add_single_config_no_dependencies(self, resolver):
        """Test adding a configuration with no dependencies."""
        config = {
            "target_table": "users",
            "schema": {"id": "IntegerField", "name": "CharField"},
        }

        resolver.add_config(config)

        assert "users" in resolver.dependency_graph
        assert resolver.dependency_graph["users"] == set()
        assert "users" in resolver.table_to_config

    def test_add_config_with_explicit_depends_on_string(self, resolver):
        """Test adding a configuration with explicit depends_on as string."""
        config = {
            "target_table": "orders",
            "depends_on": "users",
            "schema": {"id": "IntegerField", "user_id": "IntegerField"},
        }

        resolver.add_config(config)

        assert "orders" in resolver.dependency_graph
        assert resolver.dependency_graph["orders"] == {"users"}

    def test_add_config_with_explicit_depends_on_list(self, resolver):
        """Test adding a configuration with explicit depends_on as list."""
        config = {
            "target_table": "order_items",
            "depends_on": ["orders", "products"],
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(config)

        assert "order_items" in resolver.dependency_graph
        assert resolver.dependency_graph["order_items"] == {"orders", "products"}

    def test_add_config_with_foreign_key_field(self, resolver):
        """Test extracting dependencies from ForeignKeyField."""
        config = {
            "target_table": "posts",
            "schema": {
                "id": "IntegerField",
                "author_id": {"type": "ForeignKeyField", "references": "users"},
            },
        }

        resolver.add_config(config)

        assert "posts" in resolver.dependency_graph
        assert resolver.dependency_graph["posts"] == {"users"}

    def test_add_config_with_multiple_foreign_keys(self, resolver):
        """Test extracting multiple foreign key dependencies."""
        config = {
            "target_table": "order_items",
            "schema": {
                "id": "IntegerField",
                "order_id": {"type": "ForeignKeyField", "references": "orders"},
                "product_id": {"type": "ForeignKeyField", "references": "products"},
            },
        }

        resolver.add_config(config)

        assert resolver.dependency_graph["order_items"] == {"orders", "products"}

    def test_add_config_with_foreign_key_alternate_format(self, resolver):
        """Test extracting foreign key using 'foreign_key' attribute."""
        config = {
            "target_table": "comments",
            "schema": {
                "id": "IntegerField",
                "post_id": {"type": "IntegerField", "foreign_key": "posts"},
            },
        }

        resolver.add_config(config)

        assert "comments" in resolver.dependency_graph
        assert resolver.dependency_graph["comments"] == {"posts"}

    def test_add_config_mixed_dependencies(self, resolver):
        """Test combining explicit depends_on with foreign key extraction."""
        config = {
            "target_table": "reviews",
            "depends_on": "users",
            "schema": {
                "id": "IntegerField",
                "product_id": {"type": "ForeignKeyField", "references": "products"},
            },
        }

        resolver.add_config(config)

        assert resolver.dependency_graph["reviews"] == {"users", "products"}

    def test_add_config_missing_target_table(self, resolver):
        """Test that missing target_table raises ValueError."""
        config = {"schema": {"id": "IntegerField"}}

        with pytest.raises(ValueError) as exc_info:
            resolver.add_config(config)

        assert "missing 'target_table'" in str(exc_info.value).lower()

    def test_resolve_order_single_table(self, resolver):
        """Test resolving order with a single table."""
        config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        resolver.add_config(config)

        order = resolver.resolve_order()

        assert order == ["users"]

    def test_resolve_order_simple_dependency(self, resolver):
        """Test resolving order with simple dependency chain."""
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(users_config)
        resolver.add_config(posts_config)

        order = resolver.resolve_order()

        assert order == ["users", "posts"]

    def test_resolve_order_complex_dependency_chain(self, resolver):
        """Test resolving order with complex dependency chain."""
        # users -> posts -> comments
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }
        comments_config = {
            "target_table": "comments",
            "depends_on": "posts",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(users_config)
        resolver.add_config(comments_config)  # Add out of order
        resolver.add_config(posts_config)

        order = resolver.resolve_order()

        assert order == ["users", "posts", "comments"]

    def test_resolve_order_multiple_independent_tables(self, resolver):
        """Test resolving order with multiple independent tables."""
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        products_config = {
            "target_table": "products",
            "schema": {"id": "IntegerField"},
        }
        categories_config = {
            "target_table": "categories",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(products_config)
        resolver.add_config(users_config)
        resolver.add_config(categories_config)

        order = resolver.resolve_order()

        # Order should be alphabetical for independent tables
        assert set(order) == {"users", "products", "categories"}
        assert order == sorted(order)  # Alphabetical

    def test_resolve_order_diamond_dependency(self, resolver):
        """Test resolving order with diamond dependency pattern."""
        # users <- orders
        #  |         |
        #  v         v
        # posts -> order_items
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }
        orders_config = {
            "target_table": "orders",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }
        order_items_config = {
            "target_table": "order_items",
            "depends_on": ["orders", "posts"],
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(users_config)
        resolver.add_config(posts_config)
        resolver.add_config(orders_config)
        resolver.add_config(order_items_config)

        order = resolver.resolve_order()

        # Users must be first, order_items must be last
        assert order[0] == "users"
        assert order[-1] == "order_items"
        # posts and orders can be in either order
        assert set(order[1:3]) == {"posts", "orders"}

    def test_resolve_order_circular_dependency_simple(self, resolver):
        """Test that simple circular dependency is detected."""
        config_a = {
            "target_table": "table_a",
            "depends_on": "table_b",
            "schema": {"id": "IntegerField"},
        }
        config_b = {
            "target_table": "table_b",
            "depends_on": "table_a",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(config_a)
        resolver.add_config(config_b)

        with pytest.raises(CircularDependencyError) as exc_info:
            resolver.resolve_order()

        assert "circular dependency" in str(exc_info.value).lower()
        assert "table_a" in str(exc_info.value) or "table_b" in str(exc_info.value)

    def test_resolve_order_circular_dependency_complex(self, resolver):
        """Test that complex circular dependency is detected."""
        # A -> B -> C -> A
        config_a = {
            "target_table": "table_a",
            "depends_on": "table_b",
            "schema": {"id": "IntegerField"},
        }
        config_b = {
            "target_table": "table_b",
            "depends_on": "table_c",
            "schema": {"id": "IntegerField"},
        }
        config_c = {
            "target_table": "table_c",
            "depends_on": "table_a",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(config_a)
        resolver.add_config(config_b)
        resolver.add_config(config_c)

        with pytest.raises(CircularDependencyError) as exc_info:
            resolver.resolve_order()

        assert "circular dependency" in str(exc_info.value).lower()

    def test_get_ordered_configs(self, resolver):
        """Test getting configurations in dependency order."""
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(users_config, config_path="users.yaml")
        resolver.add_config(posts_config, config_path="posts.yaml")

        ordered_configs = resolver.get_ordered_configs()

        assert len(ordered_configs) == 2
        assert ordered_configs[0]["table"] == "users"
        assert ordered_configs[0]["config"] == users_config
        assert ordered_configs[0]["path"] == "users.yaml"
        assert ordered_configs[1]["table"] == "posts"
        assert ordered_configs[1]["config"] == posts_config
        assert ordered_configs[1]["path"] == "posts.yaml"

    def test_get_ordered_configs_with_external_reference(self, resolver):
        """Test behavior when a table references a non-existent dependency."""
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",  # users config not added
            "schema": {"id": "IntegerField"},
        }

        resolver.add_config(posts_config)

        ordered_configs = resolver.get_ordered_configs()

        # Should still work but only return the one config we added
        # The referenced dependency should be logged as a warning
        tables_returned = [c["table"] for c in ordered_configs]
        assert "posts" in tables_returned
        # users should be in the order but not have a config
        assert len(ordered_configs) == 1  # Only posts has a config

    def test_clear_method(self, resolver):
        """Test that clear method resets the resolver."""
        config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        resolver.add_config(config)

        assert resolver.dependency_graph != {}
        assert resolver.table_to_config != {}

        resolver.clear()

        assert resolver.dependency_graph == {}
        assert resolver.table_to_config == {}

    def test_deterministic_ordering(self, resolver):
        """Test that ordering is deterministic for tables at the same level."""
        # All independent tables
        for table_name in ["zebra", "alpha", "beta", "gamma"]:
            config = {"target_table": table_name, "schema": {"id": "IntegerField"}}
            resolver.add_config(config)

        order1 = resolver.resolve_order()
        order2 = resolver.resolve_order()

        # Should be alphabetical and deterministic
        assert order1 == order2
        assert order1 == ["alpha", "beta", "gamma", "zebra"]


class TestResolveIngestionOrderFunction:
    """Tests for the convenience function resolve_ingestion_order."""

    def test_resolve_ingestion_order_basic(self):
        """Test the convenience function with basic configs."""
        users_config = {"target_table": "users", "schema": {"id": "IntegerField"}}
        posts_config = {
            "target_table": "posts",
            "depends_on": "users",
            "schema": {"id": "IntegerField"},
        }

        configs = [posts_config, users_config]  # Intentionally out of order

        ordered = resolve_ingestion_order(configs)

        assert len(ordered) == 2
        assert ordered[0]["table"] == "users"
        assert ordered[1]["table"] == "posts"

    def test_resolve_ingestion_order_empty_list(self):
        """Test the convenience function with empty list."""
        ordered = resolve_ingestion_order([])

        assert ordered == []

    def test_resolve_ingestion_order_circular_dependency(self):
        """Test that circular dependency is detected in convenience function."""
        config_a = {
            "target_table": "table_a",
            "depends_on": "table_b",
            "schema": {"id": "IntegerField"},
        }
        config_b = {
            "target_table": "table_b",
            "depends_on": "table_a",
            "schema": {"id": "IntegerField"},
        }

        with pytest.raises(CircularDependencyError):
            resolve_ingestion_order([config_a, config_b])


class TestRealWorldScenarios:
    """Tests for real-world ingestion scenarios."""

    @pytest.fixture
    def resolver(self):
        """Create a fresh resolver instance for each test."""
        return DependencyResolver()

    def test_ecommerce_scenario(self, resolver):
        """Test ordering for a typical e-commerce database."""
        configs = [
            {"target_table": "order_items", "depends_on": ["orders", "products"]},
            {"target_table": "products", "depends_on": "categories"},
            {"target_table": "categories"},
            {"target_table": "orders", "depends_on": "customers"},
            {"target_table": "customers"},
            {"target_table": "reviews", "depends_on": ["products", "customers"]},
        ]

        for config in configs:
            if "schema" not in config:
                config["schema"] = {"id": "IntegerField"}
            resolver.add_config(config)

        order = resolver.resolve_order()

        # Verify dependencies are respected
        assert order.index("categories") < order.index("products")
        assert order.index("products") < order.index("order_items")
        assert order.index("customers") < order.index("orders")
        assert order.index("orders") < order.index("order_items")
        assert order.index("customers") < order.index("reviews")
        assert order.index("products") < order.index("reviews")

    def test_social_media_scenario(self, resolver):
        """Test ordering for a social media database."""
        configs = [
            {"target_table": "users"},
            {"target_table": "posts", "depends_on": "users"},
            {"target_table": "comments", "depends_on": ["posts", "users"]},
            {"target_table": "likes", "depends_on": ["posts", "users"]},
            {"target_table": "followers", "depends_on": ["users"]},
        ]

        for config in configs:
            if "schema" not in config:
                config["schema"] = {"id": "IntegerField"}
            resolver.add_config(config)

        order = resolver.resolve_order()

        # Users must be first
        assert order[0] == "users"
        # Posts must come before comments and likes
        assert order.index("posts") < order.index("comments")
        assert order.index("posts") < order.index("likes")

    def test_multi_tenant_scenario(self, resolver):
        """Test ordering for a multi-tenant application."""
        configs = [
            {"target_table": "tenants"},
            {"target_table": "users", "depends_on": "tenants"},
            {"target_table": "organizations", "depends_on": "tenants"},
            {"target_table": "projects", "depends_on": "organizations"},
            {"target_table": "tasks", "depends_on": ["projects", "users"]},
        ]

        for config in configs:
            if "schema" not in config:
                config["schema"] = {"id": "IntegerField"}
            resolver.add_config(config)

        order = resolver.resolve_order()

        # Tenants must be first
        assert order[0] == "tenants"
        # Organizations depends on tenants
        assert order.index("tenants") < order.index("organizations")
        # Projects depends on organizations
        assert order.index("organizations") < order.index("projects")
        # Tasks must be last (depends on both projects and users)
        assert order[-1] == "tasks"
