"""Integration tests for PostgreSQL introspector using testcontainers.

These tests spin up a real PostgreSQL instance and verify that:
1. Connection works correctly
2. Tables, columns, and constraints are introspected accurately
3. Indexes are captured correctly
4. Complex scenarios (FKs, composite keys, etc.) work

Run with: pytest tests/introspection/test_postgresql_integration.py -v -m integration
Requires Docker to be running.
"""

import subprocess
import pytest

# Check for required dependencies
try:
    import asyncpg
    from testcontainers.postgres import PostgresContainer
    HAS_DEPS = True
except ImportError:
    HAS_DEPS = False

# Skip all tests if dependencies not available
pytestmark = [
    pytest.mark.skipif(
        not HAS_DEPS,
        reason="testcontainers[postgres] or asyncpg not installed"
    ),
    pytest.mark.integration,
]


# Test schema SQL
TEST_SCHEMA_SQL = """
-- Create a simple customers dimension table
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name TEXT NOT NULL,
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE customers IS 'Customer master data';
COMMENT ON COLUMN customers.email IS 'Unique customer email';

-- Create a products dimension table
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    sku VARCHAR(50) NOT NULL UNIQUE,
    name TEXT NOT NULL,
    price NUMERIC(10, 2) NOT NULL,
    category_id INTEGER
);

-- Create a status lookup table
CREATE TABLE order_status (
    id SERIAL PRIMARY KEY,
    code VARCHAR(20) NOT NULL UNIQUE,
    name TEXT NOT NULL
);

-- Create an orders fact table
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
    status_id INTEGER REFERENCES order_status(id),
    total_amount NUMERIC(12, 2) NOT NULL,
    tax_amount NUMERIC(12, 2) DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create an order_items junction table
CREATE TABLE order_items (
    order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id INTEGER NOT NULL REFERENCES products(id),
    quantity INTEGER NOT NULL DEFAULT 1,
    unit_price NUMERIC(10, 2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

-- Create an SCD Type 2 style history table
CREATE TABLE customer_history (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    email VARCHAR(255) NOT NULL,
    valid_from TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    valid_to TIMESTAMP WITH TIME ZONE,
    is_current BOOLEAN NOT NULL DEFAULT TRUE
);

-- Create an event log table
CREATE TABLE audit_events (
    id BIGSERIAL PRIMARY KEY,
    entity_type VARCHAR(50) NOT NULL,
    entity_id INTEGER NOT NULL,
    action VARCHAR(20) NOT NULL,
    payload JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create indexes
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_created ON orders(created_at);
CREATE INDEX idx_orders_status ON orders(status_id) WHERE status_id IS NOT NULL;
CREATE INDEX idx_audit_entity ON audit_events(entity_type, entity_id);
CREATE INDEX idx_audit_created ON audit_events(created_at);
CREATE INDEX idx_customer_history_current ON customer_history(customer_id) WHERE is_current = TRUE;

-- Insert some test data for row estimates
INSERT INTO order_status (code, name) VALUES 
    ('pending', 'Pending'),
    ('confirmed', 'Confirmed'),
    ('shipped', 'Shipped'),
    ('delivered', 'Delivered'),
    ('cancelled', 'Cancelled');

INSERT INTO customers (email, name) VALUES 
    ('alice@example.com', 'Alice Smith'),
    ('bob@example.com', 'Bob Jones');

INSERT INTO products (sku, name, price) VALUES 
    ('SKU001', 'Widget A', 19.99),
    ('SKU002', 'Widget B', 29.99),
    ('SKU003', 'Widget C', 39.99);

-- Analyze tables for accurate statistics
ANALYZE;
"""


@pytest.fixture(scope="module")
def postgres_dsn(shared_postgres_dsn):
    """Reuse shared PostgreSQL container and set up schema."""
    # Run the schema setup
    shared_postgres_dsn.run_sql(TEST_SCHEMA_SQL)
    
    return shared_postgres_dsn.dsn


if HAS_DEPS:
    from flaqes.introspection.postgresql import PostgreSQLIntrospector
    from flaqes.introspection.base import IntrospectionConfig
    from flaqes.core.types import DataTypeCategory


class TestPostgreSQLConnection:
    """Tests for database connection."""

    @pytest.mark.asyncio
    async def test_connect_and_get_version(self, postgres_dsn: str) -> None:
        """Should connect and retrieve PostgreSQL version."""
        introspector = PostgreSQLIntrospector(postgres_dsn)
        
        async with introspector:
            version = await introspector._get_engine_version()
            assert version is not None
            assert "16" in version or "PostgreSQL" in version


class TestTableIntrospection:
    """Tests for table discovery."""

    @pytest.mark.asyncio
    async def test_discover_all_tables(self, postgres_dsn: str) -> None:
        """Should discover all tables in the schema."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            table_names = {t.name for t in result.graph}
            expected = {
                "customers", "products", "order_status", "orders",
                "order_items", "customer_history", "audit_events"
            }
            
            assert expected.issubset(table_names)
            assert result.table_count >= 7

    @pytest.mark.asyncio
    async def test_table_columns(self, postgres_dsn: str) -> None:
        """Should correctly introspect table columns."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            customers = result.graph.get_table_by_name("customers")
            assert customers is not None
            
            # Check column count
            assert len(customers.columns) == 5
            
            # Check specific columns
            id_col = customers.get_column("id")
            assert id_col is not None
            # SERIAL creates integer with sequence default, or is identity
            assert (
                id_col.is_identity 
                or "serial" in id_col.data_type.raw.lower()
                or (id_col.default and "nextval" in id_col.default)
            )
            
            email_col = customers.get_column("email")
            assert email_col is not None
            assert not email_col.nullable
            assert email_col.data_type.category == DataTypeCategory.TEXT
            
            metadata_col = customers.get_column("metadata")
            assert metadata_col is not None
            assert metadata_col.data_type.category == DataTypeCategory.JSON
            
            created_col = customers.get_column("created_at")
            assert created_col is not None
            assert created_col.data_type.category == DataTypeCategory.TIMESTAMP


class TestConstraintIntrospection:
    """Tests for constraint discovery."""

    @pytest.mark.asyncio
    async def test_primary_keys(self, postgres_dsn: str) -> None:
        """Should correctly introspect primary keys."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            # Simple PK
            customers = result.graph.get_table_by_name("customers")
            assert customers is not None
            assert customers.primary_key is not None
            assert customers.primary_key.columns == ("id",)
            
            # Composite PK
            order_items = result.graph.get_table_by_name("order_items")
            assert order_items is not None
            assert order_items.primary_key is not None
            assert order_items.primary_key.is_composite
            assert set(order_items.primary_key.columns) == {"order_id", "product_id"}

    @pytest.mark.asyncio
    async def test_foreign_keys(self, postgres_dsn: str) -> None:
        """Should correctly introspect foreign keys."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            orders = result.graph.get_table_by_name("orders")
            assert orders is not None
            assert len(orders.foreign_keys) == 2  # customer_id, status_id
            
            # Find customer FK
            customer_fk = next(
                (fk for fk in orders.foreign_keys if "customer_id" in fk.columns),
                None
            )
            assert customer_fk is not None
            assert customer_fk.target_table == "customers"
            # Verify on_delete is captured (should be CASCADE for customer_id FK)
            assert customer_fk.on_delete in ("CASCADE", "NO ACTION", "RESTRICT", "SET NULL")

    @pytest.mark.asyncio
    async def test_unique_constraints(self, postgres_dsn: str) -> None:
        """Should correctly introspect unique constraints."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            customers = result.graph.get_table_by_name("customers")
            assert customers is not None
            
            # Email should have unique constraint
            unique_email = any(
                c.columns == ("email",)
                for c in customers.constraints
                if hasattr(c, 'columns')
            )
            # Or captured as unique index
            if not unique_email:
                unique_email = any(
                    idx.is_unique and idx.columns == ("email",)
                    for idx in customers.indexes
                )
            assert unique_email


class TestIndexIntrospection:
    """Tests for index discovery."""

    @pytest.mark.asyncio
    async def test_indexes_discovered(self, postgres_dsn: str) -> None:
        """Should discover indexes on tables."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            orders = result.graph.get_table_by_name("orders")
            assert orders is not None
            
            # Should have at least customer_id and created_at indexes
            index_columns = {idx.columns for idx in orders.indexes if idx.columns}
            assert ("customer_id",) in index_columns or any("customer" in str(c) for c in index_columns)

    @pytest.mark.asyncio
    async def test_partial_index(self, postgres_dsn: str) -> None:
        """Should detect partial indexes."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            orders = result.graph.get_table_by_name("orders")
            assert orders is not None
            
            # Find partial index on status_id
            partial_indexes = [idx for idx in orders.indexes if idx.is_partial]
            # At least one partial index should exist
            assert len(partial_indexes) >= 1

    @pytest.mark.asyncio
    async def test_composite_index(self, postgres_dsn: str) -> None:
        """Should handle composite indexes."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            audit = result.graph.get_table_by_name("audit_events")
            assert audit is not None
            
            # Find the entity type/id composite index
            composite_indexes = [
                idx for idx in audit.indexes
                if len(idx.columns) > 1
            ]
            assert len(composite_indexes) >= 1


class TestRelationshipIntrospection:
    """Tests for relationship discovery."""

    @pytest.mark.asyncio
    async def test_relationships_built(self, postgres_dsn: str) -> None:
        """Should build relationships from FKs."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            # Should have relationships
            assert result.relationship_count > 0
            
            # Orders -> Customers relationship should exist
            order_customer_rel = any(
                r.source_table == "public.orders" and r.target_table == "public.customers"
                for r in result.graph.relationships
            )
            assert order_customer_rel

    @pytest.mark.asyncio
    async def test_identifying_relationship(self, postgres_dsn: str) -> None:
        """Should detect identifying relationships."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect()
            
            # order_items has identifying relationship to orders
            # (order_id is part of PK)
            order_items_rel = next(
                (r for r in result.graph.relationships
                 if r.source_table == "public.order_items" and r.target_table == "public.orders"),
                None
            )
            assert order_items_rel is not None
            assert order_items_rel.is_identifying


class TestSingleTableIntrospection:
    """Tests for introspect_table method."""

    @pytest.mark.asyncio
    async def test_introspect_single_table(self, postgres_dsn: str) -> None:
        """Should introspect a single table."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            table = await introspector.introspect_table("customers")
            
            assert table is not None
            assert table.name == "customers"
            assert len(table.columns) == 5

    @pytest.mark.asyncio
    async def test_introspect_nonexistent_table(self, postgres_dsn: str) -> None:
        """Should return None for nonexistent table."""
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            table = await introspector.introspect_table("nonexistent_table_xyz")
            
            # Should be None or empty result
            assert table is None


class TestIntrospectionConfig:
    """Tests for different introspection configurations."""

    @pytest.mark.asyncio
    async def test_exclude_indexes(self, postgres_dsn: str) -> None:
        """Should skip indexes when configured."""
        config = IntrospectionConfig(include_indexes=False)
        
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect(config)
            
            # Tables should have no indexes (or only PK indexes)
            for table in result.graph:
                non_pk_indexes = [idx for idx in table.indexes if not idx.is_primary]
                # With include_indexes=False, should have fewer/no indexes
                assert len(non_pk_indexes) == 0

    @pytest.mark.asyncio
    async def test_include_row_estimates(self, postgres_dsn: str) -> None:
        """Should include row estimates when configured."""
        config = IntrospectionConfig(include_row_estimates=True)
        
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect(config)
            
            # order_status has 5 rows inserted
            status = result.graph.get_table_by_name("order_status")
            if status and status.row_estimate is not None:
                assert status.row_estimate >= 0  # May be 0 before ANALYZE


class TestEdgeCases:
    """Tests for edge cases and error handling."""

    @pytest.mark.asyncio
    async def test_table_comments(self, postgres_dsn: str) -> None:
        """Should capture table comments."""
        config = IntrospectionConfig(include_comments=True)
        
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect(config)
            
            customers = result.graph.get_table_by_name("customers")
            # Comment may or may not be captured depending on implementation
            # This test verifies no error occurs
            assert customers is not None

    @pytest.mark.asyncio
    async def test_exclude_tables_pattern(self, postgres_dsn: str) -> None:
        """Should exclude tables matching patterns."""
        config = IntrospectionConfig(
            exclude_tables=("customer*",)  # Exclude customers and customer_history
        )
        
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect(config)
            
            table_names = {t.name for t in result.graph}
            # Tables matching customer* should be excluded
            assert "customers" not in table_names
            assert "customer_history" not in table_names
            # But orders should still be there
            assert "orders" in table_names

    @pytest.mark.asyncio
    async def test_include_views_config(self, postgres_dsn: str) -> None:
        """Should respect include_views configuration."""
        # By default, views are not included
        config = IntrospectionConfig(include_views=False)
        
        async with PostgreSQLIntrospector(postgres_dsn) as introspector:
            result = await introspector.introspect(config)
            # Test completes without error - validates the config path is exercised
            assert result is not None
