"""
Integration tests for full extraction workflow with PostgreSQL.

These tests verify end-to-end extraction with a real database,
including FK traversal, filtering, cycles, and anonymization.
"""

import pytest

from dbslice.config import SeedSpec, TraversalDirection
from dbslice.core.engine import ExtractionEngine

pytestmark = pytest.mark.integration


class TestBasicExtraction:
    """Test basic extraction scenarios with FK traversal."""

    def test_extract_single_order_with_parents(
        self, ecommerce_schema: dict, extract_config_factory
    ):
        """
        Test extracting a single order with all parent records.

        Should extract:
        - Order #1
        - User #1 (order's user)
        - Order items for order #1 (items 1, 2)
        - Products referenced by those items (products 1, 2)
        """
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.UP,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Verify extraction statistics
        assert result.total_rows() > 0
        assert "orders" in result.tables
        assert "users" in result.tables
        assert "order_items" in result.tables
        assert "products" in result.tables

        # Verify specific data
        assert len(result.tables["orders"]) == 1
        assert result.tables["orders"][0]["id"] == 1

        # User #1 should be included
        assert len(result.tables["users"]) == 1
        assert result.tables["users"][0]["id"] == 1

        # Order items 1 and 2 should be included
        assert len(result.tables["order_items"]) == 2
        item_ids = {item["id"] for item in result.tables["order_items"]}
        assert item_ids == {1, 2}

        # Products 1 and 2 should be included
        assert len(result.tables["products"]) == 2
        product_ids = {p["id"] for p in result.tables["products"]}
        assert product_ids == {1, 2}

        # Reviews should NOT be included (only going UP)
        assert "reviews" not in result.tables or len(result.tables["reviews"]) == 0

    def test_extract_user_with_children(self, ecommerce_schema: dict, extract_config_factory):
        """
        Test extracting a user with all child records (orders, reviews).

        Should extract:
        - User #1
        - Orders by user #1 (orders 1, 2)
        - Order items for those orders
        - Products referenced by order items
        - Reviews by user #1
        """
        config = extract_config_factory(
            seeds=[SeedSpec.parse("users.id=1")],
            direction=TraversalDirection.DOWN,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Verify user
        assert len(result.tables["users"]) == 1
        assert result.tables["users"][0]["id"] == 1

        # User #1 has orders 1 and 2
        assert len(result.tables["orders"]) == 2
        order_ids = {o["id"] for o in result.tables["orders"]}
        assert order_ids == {1, 2}

        # Order items for orders 1 and 2
        assert len(result.tables["order_items"]) == 3
        item_ids = {item["id"] for item in result.tables["order_items"]}
        assert item_ids == {1, 2, 3}

        # User #1's reviews (reviews 1, 2)
        assert len(result.tables["reviews"]) == 2
        review_ids = {r["id"] for r in result.tables["reviews"]}
        assert review_ids == {1, 2}

    def test_extract_bidirectional(self, ecommerce_schema: dict, extract_config_factory):
        """
        Test bidirectional extraction (both parents and children).

        Starting from order_items, should extract:
        - The order item itself
        - UP: order, user, product
        - DOWN: nothing (order_items is a leaf in this direction)
        """
        config = extract_config_factory(
            seeds=[SeedSpec.parse("order_items.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Verify the order item
        assert len(result.tables["order_items"]) == 1
        assert result.tables["order_items"][0]["id"] == 1

        # Should have order #1
        assert len(result.tables["orders"]) == 1
        assert result.tables["orders"][0]["id"] == 1

        # Should have user #1 (order's user)
        assert len(result.tables["users"]) == 1
        assert result.tables["users"][0]["id"] == 1

        # Should have product #1 (item's product)
        assert len(result.tables["products"]) == 1
        assert result.tables["products"][0]["id"] == 1


class TestWhereClauseSeeds:
    """Test extraction with WHERE clause seeds."""

    def test_extract_with_status_filter(self, ecommerce_schema: dict, extract_config_factory):
        """Test extracting orders with status filter."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders:status='completed'")],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should extract orders 1 and 3 (both completed)
        assert len(result.tables["orders"]) == 2
        order_ids = {o["id"] for o in result.tables["orders"]}
        assert order_ids == {1, 3}

        # Should have users 1 and 2
        user_ids = {u["id"] for u in result.tables["users"]}
        assert user_ids == {1, 2}

    def test_extract_with_date_filter(self, ecommerce_schema: dict, extract_config_factory):
        """Test extracting orders with date filter."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders:created_at >= '2024-01-03'")],
            direction=TraversalDirection.UP,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should extract orders 3 and 4 (Jan 3 and Jan 4)
        assert len(result.tables["orders"]) == 2
        order_ids = {o["id"] for o in result.tables["orders"]}
        assert order_ids == {3, 4}

    def test_extract_with_numeric_comparison(self, ecommerce_schema: dict, extract_config_factory):
        """Test extracting products with price filter."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("products:price > 20.00")],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should extract products 2 and 3 (49.99 and 29.99)
        assert len(result.tables["products"]) == 2
        product_ids = {p["id"] for p in result.tables["products"]}
        assert product_ids == {2, 3}


class TestMultipleSeeds:
    """Test extraction with multiple seed specifications."""

    def test_multiple_orders(self, ecommerce_schema: dict, extract_config_factory):
        """Test extracting multiple orders by ID."""
        config = extract_config_factory(
            seeds=[
                SeedSpec.parse("orders.id=1"),
                SeedSpec.parse("orders.id=3"),
            ],
            direction=TraversalDirection.UP,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should extract orders 1 and 3
        assert len(result.tables["orders"]) == 2
        order_ids = {o["id"] for o in result.tables["orders"]}
        assert order_ids == {1, 3}

        # Should extract users 1 and 2
        user_ids = {u["id"] for u in result.tables["users"]}
        assert user_ids == {1, 2}

    def test_multiple_tables(self, ecommerce_schema: dict, extract_config_factory):
        """Test extracting from multiple different tables."""
        config = extract_config_factory(
            seeds=[
                SeedSpec.parse("users.id=2"),
                SeedSpec.parse("products.id=4"),
            ],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should have user #2
        assert any(u["id"] == 2 for u in result.tables["users"])

        # Should have product #4
        assert any(p["id"] == 4 for p in result.tables["products"])

    def test_overlapping_seeds(self, ecommerce_schema: dict, extract_config_factory):
        """Test that overlapping seeds don't cause duplicates."""
        config = extract_config_factory(
            seeds=[
                SeedSpec.parse("orders.id=1"),
                SeedSpec.parse("users.id=1"),  # User #1 is also order #1's user
            ],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should have exactly one copy of user #1
        user_1_count = sum(1 for u in result.tables["users"] if u["id"] == 1)
        assert user_1_count == 1


class TestCycleHandling:
    """Test handling of circular foreign key references."""

    def test_detect_cycles(self, circular_ref_schema: dict, extract_config_factory):
        """Test that circular references are detected."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("employees.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should detect cycles
        assert result.has_cycles
        assert len(result.broken_fks) > 0
        assert len(result.cycle_infos) > 0

    def test_cycle_resolution(self, circular_ref_schema: dict, extract_config_factory):
        """Test that cycles are resolved with nullable FK breaking."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("departments.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should break cycles and create deferred updates
        assert result.has_cycles
        assert len(result.broken_fks) > 0
        assert len(result.deferred_updates) > 0

        # Should still extract all related records
        assert "departments" in result.tables
        assert "employees" in result.tables

        # Verify insert order is valid (no circular dependencies)
        assert len(result.insert_order) > 0

    def test_self_referential_extraction(self, circular_ref_schema: dict, extract_config_factory):
        """Test extraction with self-referential table (employees.manager_id)."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("employees.id=2")],  # Bob, managed by Alice
            direction=TraversalDirection.UP,
            depth=10,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should extract both Bob and his manager Alice
        assert len(result.tables["employees"]) == 2
        emp_ids = {e["id"] for e in result.tables["employees"]}
        assert emp_ids == {1, 2}  # Alice and Bob


class TestAnonymization:
    """Test end-to-end anonymization."""

    def test_anonymize_sensitive_fields(self, ecommerce_schema: dict, extract_config_factory):
        """Test that sensitive fields are anonymized."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("users.id=1")],
            direction=TraversalDirection.DOWN,
            depth=10,
            anonymize=True,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Get user data
        user = result.tables["users"][0]

        # Email should be anonymized (not the original)
        assert user["email"] != "alice@example.com"
        assert "@" in user["email"]  # Should still look like an email

        # Name should be anonymized
        assert user["name"] != "Alice Smith"

        # Phone should be anonymized
        assert user["phone"] != "555-0001"

    def test_anonymization_with_redact_fields(self, ecommerce_schema: dict, extract_config_factory):
        """Test custom field redaction."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("products.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            anonymize=True,
            redact_fields=["products.description"],
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Product description should be redacted
        product = result.tables["products"][0]
        assert product["description"] != "A useful widget"

    def test_anonymization_preserves_relationships(
        self, ecommerce_schema: dict, extract_config_factory
    ):
        """Test that anonymization preserves FK relationships."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            anonymize=True,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Get order and verify user_id FK is preserved
        order = result.tables["orders"][0]
        user = result.tables["users"][0]

        assert order["user_id"] == user["id"]

        # User data should be anonymized
        assert user["email"] != "alice@example.com"


class TestValidation:
    """Test extraction validation for referential integrity."""

    def test_validation_passes(self, ecommerce_schema: dict, extract_config_factory):
        """Test that valid extraction passes validation."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            validate=True,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Validation should pass
        assert result.validation_result is not None
        assert result.validation_result.is_valid
        assert len(result.validation_result.orphaned_records) == 0

    def test_validation_with_cycles(self, circular_ref_schema: dict, extract_config_factory):
        """Test that validation handles circular references correctly."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("employees.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            validate=True,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Validation should pass even with cycles (broken FKs are tracked)
        assert result.validation_result is not None
        assert result.validation_result.is_valid

    def test_validation_fail_on_error(
        self, ecommerce_schema: dict, extract_config_factory, pg_connection
    ):
        """Test that validation can fail the extraction if configured."""
        # This test is tricky because we need to create an invalid extraction
        # For now, we just verify the flag works
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.UP,
            depth=10,
            validate=True,
            fail_on_validation_error=True,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should succeed with valid data
        assert result.validation_result is not None
        assert result.validation_result.is_valid


class TestTraversalDepth:
    """Test FK traversal depth limiting."""

    def test_depth_zero(self, ecommerce_schema: dict, extract_config_factory):
        """Test that depth=0 only extracts seed rows."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.BOTH,
            depth=0,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should only have the order itself
        assert len(result.tables["orders"]) == 1
        assert result.tables["orders"][0]["id"] == 1

        # Should not have related tables
        assert "users" not in result.tables or len(result.tables["users"]) == 0
        assert "order_items" not in result.tables or len(result.tables["order_items"]) == 0

    def test_depth_one(self, ecommerce_schema: dict, extract_config_factory):
        """Test that depth=1 extracts one level of relationships."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.UP,
            depth=1,
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should have order and direct parents (user, order_items)
        assert "orders" in result.tables
        assert "users" in result.tables
        assert "order_items" in result.tables

        # Should NOT have products (depth 2)
        assert "products" not in result.tables or len(result.tables["products"]) == 0


class TestExcludeTables:
    """Test table exclusion from extraction."""

    def test_exclude_table(self, ecommerce_schema: dict, extract_config_factory):
        """Test excluding a table from extraction."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("users.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            exclude_tables={"reviews"},
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should have user and orders
        assert "users" in result.tables
        assert "orders" in result.tables

        # Should NOT have reviews
        assert "reviews" not in result.tables

    def test_exclude_multiple_tables(self, ecommerce_schema: dict, extract_config_factory):
        """Test excluding multiple tables."""
        config = extract_config_factory(
            seeds=[SeedSpec.parse("orders.id=1")],
            direction=TraversalDirection.BOTH,
            depth=10,
            exclude_tables={"reviews", "products"},
        )

        engine = ExtractionEngine(config)
        result, schema = engine.extract()

        # Should have order and user
        assert "orders" in result.tables
        assert "users" in result.tables

        # Should NOT have excluded tables
        assert "reviews" not in result.tables
        assert "products" not in result.tables
