"""
Unit tests for ETL configuration models (config_models.py).

Tests Pydantic model validation and parsing for:
- Extract configs (HTTP, file, database, cloud storage)
- Load configs (Postgres, SQLite, file, cloud storage)
- Transform configs (simple ops, JSONata, custom function)
- Validation configs (extract, load)
- Settings config
"""

import pytest
from pydantic import ValidationError

from pycharter.etl_generator.config_models import (  # Enums; Config models; Parsers
    CloudStorageExtractConfig,
    CloudStorageLoadConfig,
    ContractRef,
    DatabaseExtractConfig,
    DLQBackend,
    DLQConfig,
    ExtractType,
    ExtractValidationConfig,
    FileExtractConfig,
    FileFormat,
    FileLoadConfig,
    HTTPExtractConfig,
    LoadType,
    LoadValidationConfig,
    OnErrorAction,
    PostgresLoadConfig,
    SettingsConfig,
    SQLiteLoadConfig,
    TransformConfig,
    WriteMethod,
    parse_extract_config,
    parse_load_config,
    parse_settings_config,
    parse_transform_config,
)


class TestExtractConfigs:
    """Test extract configuration models."""

    def test_http_extract_basic(self):
        """Test basic HTTP extract config."""
        config = HTTPExtractConfig(url="https://api.example.com/data")
        assert config.type == "http"
        assert config.url == "https://api.example.com/data"
        assert config.method.value == "GET"  # default
        assert config.batch_size == 1000  # default

    def test_http_extract_with_all_options(self):
        """Test HTTP extract config with all options."""
        config = HTTPExtractConfig(
            url="https://api.example.com/data",
            method="POST",
            headers={"Authorization": "Bearer token"},
            params={"page": 1},
            body={"filter": "active"},
            response_format="json",
            batch_size=500,
        )
        assert config.method.value == "POST"
        assert config.headers == {"Authorization": "Bearer token"}
        assert config.params == {"page": 1}

    def test_file_extract_basic(self):
        """Test basic file extract config."""
        config = FileExtractConfig(path="./data/input.csv")
        assert config.type == "file"
        assert config.path == "./data/input.csv"
        assert config.format is None  # auto-detect

    def test_file_extract_with_format(self):
        """Test file extract config with explicit format."""
        config = FileExtractConfig(
            path="./data/input.json",
            format="json",
            batch_size=100,
        )
        assert config.format == FileFormat.JSON

    def test_database_extract_basic(self):
        """Test basic database extract config."""
        config = DatabaseExtractConfig(
            query="SELECT * FROM users",
            connection_string="postgresql://localhost/db",
        )
        assert config.type == "database"
        assert config.query == "SELECT * FROM users"

    def test_parse_extract_config_http(self):
        """Test parse_extract_config with HTTP type."""
        config = parse_extract_config(
            {"type": "http", "url": "https://api.example.com"}
        )
        assert isinstance(config, HTTPExtractConfig)
        assert config.url == "https://api.example.com"

    def test_parse_extract_config_file(self):
        """Test parse_extract_config with file type."""
        config = parse_extract_config({"type": "file", "path": "./data.json"})
        assert isinstance(config, FileExtractConfig)
        assert config.path == "./data.json"

    def test_parse_extract_config_database(self):
        """Test parse_extract_config with database type."""
        config = parse_extract_config(
            {
                "type": "database",
                "query": "SELECT * FROM users",
                "connection_string": "postgresql://localhost/db",
            }
        )
        assert isinstance(config, DatabaseExtractConfig)

    def test_parse_extract_config_invalid_type(self):
        """Test parse_extract_config with invalid type."""
        with pytest.raises(ValueError, match="Unknown extract type"):
            parse_extract_config({"type": "invalid"})


class TestLoadConfigs:
    """Test load configuration models."""

    def test_postgres_load_basic(self):
        """Test basic Postgres load config."""
        config = PostgresLoadConfig(
            type="postgres",
            table="users",
            connection_string="postgresql://localhost/db",
        )
        assert config.type == "postgres"
        assert config.table == "users"
        assert config.write_method == WriteMethod.INSERT  # default

    def test_postgres_load_upsert(self):
        """Test Postgres load config with upsert."""
        config = PostgresLoadConfig(
            type="postgres",
            table="users",
            connection_string="postgresql://localhost/db",
            write_method="upsert",
            primary_key="user_id",
            update_columns=["name", "email"],
        )
        assert config.write_method == WriteMethod.UPSERT
        assert config.primary_key == "user_id"

    def test_file_load_basic(self):
        """Test basic file load config."""
        config = FileLoadConfig(path="./output/data.json")
        assert config.type == "file"
        assert config.path == "./output/data.json"
        assert config.format == FileFormat.JSON  # default

    def test_parse_load_config_postgres(self):
        """Test parse_load_config with postgres type."""
        config = parse_load_config(
            {
                "type": "postgres",
                "table": "users",
                "connection_string": "postgresql://localhost/db",
            }
        )
        assert isinstance(config, PostgresLoadConfig)

    def test_parse_load_config_file(self):
        """Test parse_load_config with file type."""
        config = parse_load_config(
            {
                "type": "file",
                "path": "./output.json",
            }
        )
        assert isinstance(config, FileLoadConfig)

    def test_parse_load_config_invalid_type(self):
        """Test parse_load_config with invalid type."""
        with pytest.raises(ValueError, match="Unknown load type"):
            parse_load_config({"type": "invalid"})


class TestTransformConfig:
    """Test transform configuration models."""

    def test_transform_config_simple_ops(self):
        """Test transform config with simple operations."""
        config = TransformConfig(
            transform={
                "rename": {"old_name": "new_name"},
                "convert": {"age": "int"},
            }
        )
        assert config.transform is not None
        assert config.jsonata is None

    def test_transform_config_jsonata(self):
        """Test transform config with JSONata."""
        config = TransformConfig(
            jsonata={
                "expression": '{ "name": name, "upper": $uppercase(name) }',
                "mode": "record",
            }
        )
        assert config.jsonata is not None
        assert config.jsonata.mode.value == "record"

    def test_transform_config_combined(self):
        """Test transform config with both simple ops and JSONata."""
        config = TransformConfig(
            transform={"rename": {"old": "new"}},
            jsonata={"expression": "$", "mode": "batch"},
        )
        assert config.transform is not None
        assert config.jsonata is not None


class TestValidationConfigs:
    """Test validation configuration models."""

    def test_extract_validation_config_basic(self):
        """Test basic extract validation config."""
        config = ExtractValidationConfig(
            schema_path="./source_schema.yaml",
            on_error="quarantine",
        )
        assert config.schema_path == "./source_schema.yaml"
        assert config.on_error == OnErrorAction.QUARANTINE

    def test_extract_validation_config_with_dlq(self):
        """Test extract validation config with DLQ."""
        config = ExtractValidationConfig(
            schema_path="./source.yaml",
            on_error="quarantine",
            dlq=DLQConfig(enabled=True, backend="file", path="./dlq"),
        )
        assert config.dlq is not None
        assert config.dlq.backend == DLQBackend.FILE

    def test_load_validation_config_basic(self):
        """Test basic load validation config."""
        config = LoadValidationConfig(
            contract="./contracts/orders",
            on_error="fail",
        )
        assert config.contract == "./contracts/orders"
        assert config.on_error == OnErrorAction.FAIL

    def test_load_validation_config_database_ref(self):
        """Test load validation config with database contract ref."""
        config = LoadValidationConfig(
            contract=ContractRef(name="orders", version="2.0"),
            on_error="skip",
        )
        assert isinstance(config.contract, ContractRef)
        assert config.contract.name == "orders"

    def test_load_validation_use_contract(self):
        """Test load validation with use_contract flag."""
        config = LoadValidationConfig(use_contract=True, on_error="warn")
        assert config.use_contract is True
        assert config.contract is None


class TestSettingsConfig:
    """Test settings configuration model."""

    def test_settings_config_basic(self):
        """Test basic settings config."""
        config = SettingsConfig(name="my_pipeline")
        assert config.name == "my_pipeline"
        assert config.dlq is None
        assert config.metadata_store is None

    def test_settings_config_with_dlq(self):
        """Test settings config with DLQ."""
        config = SettingsConfig(
            name="my_pipeline",
            dlq=DLQConfig(enabled=True, backend="file", path="./dlq"),
        )
        assert config.dlq is not None
        assert config.dlq.enabled is True

    def test_settings_config_full(self):
        """Test settings config with all options."""
        config = SettingsConfig(
            name="my_pipeline",
            dlq=DLQConfig(
                enabled=True, backend="database", connection_string="postgresql://..."
            ),
            metadata_store={
                "type": "postgres",
                "connection_string": "postgresql://...",
            },
            contract="orders_v2",
        )
        assert config.dlq is not None
        assert config.metadata_store is not None
        assert config.contract == "orders_v2"


class TestDLQConfig:
    """Test DLQ configuration model."""

    def test_dlq_config_file(self):
        """Test file-backed DLQ config."""
        config = DLQConfig(enabled=True, backend="file", path="./dlq")
        assert config.enabled is True
        assert config.backend == DLQBackend.FILE
        assert config.path == "./dlq"

    def test_dlq_config_database(self):
        """Test database-backed DLQ config."""
        config = DLQConfig(
            enabled=True,
            backend="database",
            connection_string="postgresql://localhost/db",
            schema_name="pycharter",
            table="dead_letter_queue",
        )
        assert config.backend == DLQBackend.DATABASE
        assert config.connection_string == "postgresql://localhost/db"
        assert config.schema_name == "pycharter"

    def test_dlq_config_disabled(self):
        """Test disabled DLQ config."""
        config = DLQConfig(enabled=False)
        assert config.enabled is False


class TestParserFunctions:
    """Test parser helper functions."""

    def test_parse_settings_config(self):
        """Test parse_settings_config function."""
        config = parse_settings_config(
            {
                "name": "test_pipeline",
                "dlq": {"enabled": True, "backend": "file", "path": "./dlq"},
            }
        )
        assert isinstance(config, SettingsConfig)
        assert config.name == "test_pipeline"

    def test_parse_transform_config_list(self):
        """Test parse_transform_config with list input."""
        config = parse_transform_config(
            [
                {"rename": {"old": "new"}},
                {"convert": {"age": "int"}},
            ]
        )
        assert config is not None
        assert config.transform == [
            {"rename": {"old": "new"}},
            {"convert": {"age": "int"}},
        ]

    def test_parse_transform_config_none(self):
        """Test parse_transform_config with None input."""
        config = parse_transform_config(None)
        assert config is None
