"""
Tests for Schema Drift Detector.
"""

import pytest

from atomsql import Database
from atomsql.orm import Model, fields

from atomingest.schema.drift_detector import (
    SchemaDriftDetector,
    DriftType,
    DriftIssue,
    DriftReport,
    detect_drift,
)


class TestDriftReport:
    """Tests for DriftReport class."""

    def test_empty_report(self):
        """Test empty report with no issues."""
        report = DriftReport()

        assert not report.has_drift
        assert report.drift_count == 0
        assert "No schema drift detected" in str(report)

    def test_report_with_issues(self):
        """Test report with drift issues."""
        report = DriftReport(
            issues=[
                DriftIssue(
                    drift_type=DriftType.MISSING_TABLE,
                    table_name="users",
                ),
                DriftIssue(
                    drift_type=DriftType.MISSING_COLUMN,
                    table_name="orders",
                    column_name="status",
                ),
            ],
            tables_checked=["users", "orders"],
        )

        assert report.has_drift
        assert report.drift_count == 2
        assert "2 issue(s) found" in str(report)

    def test_issues_by_type(self):
        """Test filtering issues by type."""
        report = DriftReport(
            issues=[
                DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="users"),
                DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="orders"),
                DriftIssue(
                    drift_type=DriftType.MISSING_COLUMN,
                    table_name="products",
                    column_name="price",
                ),
            ]
        )

        missing_tables = report.issues_by_type(DriftType.MISSING_TABLE)
        assert len(missing_tables) == 2

        missing_columns = report.issues_by_type(DriftType.MISSING_COLUMN)
        assert len(missing_columns) == 1

    def test_issues_by_table(self):
        """Test filtering issues by table."""
        report = DriftReport(
            issues=[
                DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="users"),
                DriftIssue(
                    drift_type=DriftType.MISSING_COLUMN,
                    table_name="users",
                    column_name="email",
                ),
                DriftIssue(
                    drift_type=DriftType.TYPE_MISMATCH,
                    table_name="users",
                    column_name="age",
                ),
            ]
        )

        user_issues = report.issues_by_table("users")
        assert len(user_issues) == 3

    def test_summary(self):
        """Test drift summary by type."""
        report = DriftReport(
            issues=[
                DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="t1"),
                DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="t2"),
                DriftIssue(
                    drift_type=DriftType.TYPE_MISMATCH,
                    table_name="t3",
                    column_name="c1",
                ),
            ]
        )

        summary = report.summary()
        assert summary["missing_table"] == 2
        assert summary["type_mismatch"] == 1
        assert summary["missing_column"] == 0


class TestDriftIssue:
    """Tests for DriftIssue class."""

    def test_missing_table_message(self):
        """Test missing table issue message."""
        issue = DriftIssue(drift_type=DriftType.MISSING_TABLE, table_name="users")

        message = str(issue)
        assert "users" in message
        assert "missing in database" in message

    def test_missing_column_message(self):
        """Test missing column issue message."""
        issue = DriftIssue(
            drift_type=DriftType.MISSING_COLUMN,
            table_name="users",
            column_name="email",
        )

        message = str(issue)
        assert "users" in message
        assert "email" in message
        assert "missing in database" in message

    def test_type_mismatch_message(self):
        """Test type mismatch issue message."""
        issue = DriftIssue(
            drift_type=DriftType.TYPE_MISMATCH,
            table_name="users",
            column_name="age",
            expected="INTEGER",
            actual="TEXT",
        )

        message = str(issue)
        assert "users" in message
        assert "age" in message
        assert "INTEGER" in message
        assert "TEXT" in message


class TestSchemaDriftDetector:
    """Tests for SchemaDriftDetector class."""

    @pytest.fixture
    def db(self):
        """Create an in-memory database for testing."""
        database = Database("sqlite://:memory:")
        yield database
        database.close()

    @pytest.fixture
    def yaml_files(self, tmp_path):
        """Create temporary YAML files for testing."""
        files = {}

        # Users table YAML
        users_yaml = tmp_path / "users.yaml"
        users_yaml.write_text(
            """
target_table: users
schema:
  name:
    type: CharField
    max_length: 255
  email: EmailField
  age: IntegerField
"""
        )
        files["users"] = str(users_yaml)

        # Posts table YAML
        posts_yaml = tmp_path / "posts.yaml"
        posts_yaml.write_text(
            """
target_table: posts
schema:
  title:
    type: CharField
    max_length: 255
  content: TextField
  published: BooleanField
"""
        )
        files["posts"] = str(posts_yaml)

        return files

    def test_no_drift_when_schema_matches(self, db, yaml_files):
        """Test that no drift is detected when schema matches exactly."""

        # Create tables that match YAML
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.IntegerField()

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert not report.has_drift
        assert report.drift_count == 0

    def test_detect_missing_table(self, db, yaml_files):
        """Test detection of missing table."""
        # Don't create the users table
        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert report.has_drift
        assert report.drift_count == 1

        issue = report.issues[0]
        assert issue.drift_type == DriftType.MISSING_TABLE
        assert issue.table_name == "users"

    def test_detect_missing_column(self, db, yaml_files):
        """Test detection of missing column."""

        # Create table with missing column
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            # Missing: age field

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert report.has_drift
        missing_col_issues = report.issues_by_type(DriftType.MISSING_COLUMN)
        assert len(missing_col_issues) == 1
        assert missing_col_issues[0].column_name == "age"

    def test_detect_extra_column(self, db, yaml_files):
        """Test detection of extra column in database."""

        # Create table with extra column
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.IntegerField()
            phone = fields.CharField(max_length=20)  # Extra column

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert report.has_drift
        extra_col_issues = report.issues_by_type(DriftType.EXTRA_COLUMN)
        assert len(extra_col_issues) == 1
        assert extra_col_issues[0].column_name == "phone"

    def test_detect_type_mismatch(self, db, yaml_files):
        """Test detection of type mismatch."""

        # Create table with wrong type
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.CharField(max_length=10)  # Should be IntegerField

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert report.has_drift
        type_issues = report.issues_by_type(DriftType.TYPE_MISMATCH)
        assert len(type_issues) == 1
        assert type_issues[0].column_name == "age"
        assert "INTEGER" in type_issues[0].expected
        assert (
            "INTEGER" not in type_issues[0].actual
        )  # CharField maps to VARCHAR in SQLite

    def test_detect_nullable_mismatch(self, db, tmp_path):
        """Test detection of nullable constraint mismatch."""

        # Create YAML with nullable field
        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """
target_table: test_table
schema:
  name:
    type: CharField
    max_length: 255
    nullable: true
"""
        )

        # Create table with NOT NULL
        class TestModel(Model):
            name = fields.CharField(max_length=255, nullable=False)

        TestModel._table_name = "test_table"
        db.register(TestModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        assert report.has_drift
        nullable_issues = report.issues_by_type(DriftType.NULLABLE_MISMATCH)
        assert len(nullable_issues) == 1
        assert nullable_issues[0].column_name == "name"

    def test_detect_multiple_issues(self, db, yaml_files):
        """Test detection of multiple drift issues."""

        # Create table with multiple problems
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            # Missing: email
            age = fields.CharField(max_length=10)  # Wrong type
            extra_field = fields.TextField()  # Extra column

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        assert report.has_drift
        assert report.drift_count >= 3  # At least 3 issues

        # Check for each type of issue
        assert len(report.issues_by_type(DriftType.MISSING_COLUMN)) >= 1
        assert len(report.issues_by_type(DriftType.TYPE_MISMATCH)) >= 1
        assert len(report.issues_by_type(DriftType.EXTRA_COLUMN)) >= 1

    def test_multiple_yaml_files(self, db, yaml_files):
        """Test drift detection across multiple YAML files."""

        # Create only users table, not posts
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.IntegerField()

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"], yaml_files["posts"]])

        assert report.has_drift
        assert len(report.tables_checked) == 2

        # Users should have no issues, posts should be missing
        users_issues = report.issues_by_table("users")
        posts_issues = report.issues_by_table("posts")

        assert len(users_issues) == 0
        assert len(posts_issues) > 0
        assert any(
            issue.drift_type == DriftType.MISSING_TABLE for issue in posts_issues
        )

    def test_check_extra_tables(self, db, yaml_files):
        """Test detection of extra tables in database."""

        # Create users table as expected
        class UserModel(Model):
            _table_name = "users"
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.IntegerField()

        # Create extra table not in YAML
        class ExtraModel(Model):
            data = fields.TextField()

        db.register(UserModel)
        db.register(ExtraModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]], check_extra_tables=True)

        assert report.has_drift
        extra_table_issues = report.issues_by_type(DriftType.EXTRA_TABLE)
        assert len(extra_table_issues) >= 1
        # ExtraModel has table name 'extramodel'
        assert any("extra" in issue.table_name.lower() for issue in extra_table_issues)

    def test_ignore_id_field(self, db, yaml_files):
        """Test that auto-generated id field is ignored."""

        # YAML doesn't define 'id', but it's auto-generated
        class UserModel(Model):
            name = fields.CharField(max_length=255)
            email = fields.EmailField()
            age = fields.IntegerField()

        UserModel._table_name = "users"
        db.register(UserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([yaml_files["users"]])

        # Should not report 'id' as extra column
        assert not report.has_drift
        extra_cols = report.issues_by_type(DriftType.EXTRA_COLUMN)
        assert not any(issue.column_name == "id" for issue in extra_cols)

    def test_encrypted_fields(self, db, tmp_path):
        """Test drift detection with encrypted fields."""

        yaml_file = tmp_path / "encrypted.yaml"
        yaml_file.write_text(
            """
target_table: secure_data
schema:
  username: CharField
  password: EncryptedCharField
  ssn: EncryptedStringField
"""
        )

        # Encrypted fields are stored as TEXT
        class SecureModel(Model):
            _table_name = "secure_data"
            username = fields.CharField(max_length=255)
            password = fields.TextField()  # Encrypted fields use TEXT
            ssn = fields.TextField()

        db.register(SecureModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        # Should not report type mismatches for encrypted fields
        assert not report.has_drift

    def test_type_matching_variants(self, db, tmp_path):
        """Test that type matching handles variants correctly."""

        yaml_file = tmp_path / "variants.yaml"
        yaml_file.write_text(
            """
target_table: variants
schema:
  text_field: TextField
  int_field: IntegerField
"""
        )

        class VariantModel(Model):
            text_field = fields.TextField()
            int_field = fields.IntegerField()

        VariantModel._table_name = "variants"
        db.register(VariantModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        # VARCHAR should match TEXT, INT should match INTEGER
        assert not report.has_drift


class TestDetectDriftFunction:
    """Tests for the convenience function."""

    @pytest.fixture
    def db(self):
        """Create an in-memory database for testing."""
        database = Database("sqlite://:memory:")
        yield database
        database.close()

    def test_convenience_function(self, db, tmp_path):
        """Test the convenience function works correctly."""

        yaml_file = tmp_path / "test.yaml"
        yaml_file.write_text(
            """
target_table: test_table
schema:
  name:
    type: CharField
    max_length: 255
"""
        )

        # Don't create the table - should detect drift
        report = detect_drift(db, [str(yaml_file)])

        assert report.has_drift
        assert report.drift_count == 1


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

    @pytest.fixture
    def db(self):
        """Create an in-memory database for testing."""
        database = Database("sqlite://:memory:")
        yield database
        database.close()

    def test_schema_evolution(self, db, tmp_path):
        """Test detecting schema that evolved over time."""

        yaml_file = tmp_path / "evolved.yaml"
        yaml_file.write_text(
            """
target_table: users
schema:
  username:
    type: CharField
    max_length: 255
  email: EmailField
  created_at: DateTimeField
  updated_at: DateTimeField
  is_active: BooleanField
"""
        )

        # Old schema - missing new fields
        class OldUserModel(Model):
            username = fields.CharField(max_length=255)
            email = fields.EmailField()

        OldUserModel._table_name = "users"
        db.register(OldUserModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        assert report.has_drift
        missing_columns = report.issues_by_type(DriftType.MISSING_COLUMN)
        assert len(missing_columns) == 3  # created_at, updated_at, is_active

        missing_names = {issue.column_name for issue in missing_columns}
        assert "created_at" in missing_names
        assert "updated_at" in missing_names
        assert "is_active" in missing_names

    def test_partial_migration(self, db, tmp_path):
        """Test detecting partially migrated schema."""

        yaml_file = tmp_path / "partial.yaml"
        yaml_file.write_text(
            """
target_table: products
schema:
  name:
    type: CharField
    max_length: 255
  description: TextField
  price: FloatField
  stock: IntegerField
"""
        )

        # Some fields migrated, some not
        class PartialModel(Model):
            name = fields.CharField(max_length=255)
            description = fields.TextField()
            price = fields.CharField(max_length=20)  # Wrong type (not migrated)
            # Missing: stock

        PartialModel._table_name = "products"
        db.register(PartialModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        assert report.has_drift

        # Should detect both missing column and type mismatch
        missing = report.issues_by_type(DriftType.MISSING_COLUMN)
        type_mismatch = report.issues_by_type(DriftType.TYPE_MISMATCH)

        assert len(missing) == 1
        assert missing[0].column_name == "stock"

        assert len(type_mismatch) == 1
        assert type_mismatch[0].column_name == "price"

    def test_development_vs_production_drift(self, db, tmp_path):
        """Test detecting drift between dev and prod environments."""

        yaml_file = tmp_path / "prod_schema.yaml"
        yaml_file.write_text(
            """
target_table: orders
schema:
  order_id:
    type: CharField
    max_length: 255
  customer_id:
    type: CharField
    max_length: 255
  total: FloatField
  status:
    type: CharField
    max_length: 50
  payment_method:
    type: CharField
    max_length: 50
"""
        )

        # Dev has extra debugging fields
        class DevOrderModel(Model):
            order_id = fields.CharField(max_length=255)
            customer_id = fields.CharField(max_length=255)
            total = fields.FloatField()
            status = fields.CharField(max_length=50)
            payment_method = fields.CharField(max_length=50)
            debug_info = fields.JSONField()  # Extra field in dev
            internal_notes = fields.TextField()  # Extra field in dev

        DevOrderModel._table_name = "orders"
        db.register(DevOrderModel)

        detector = SchemaDriftDetector(db)
        report = detector.detect_drift([str(yaml_file)])

        assert report.has_drift
        extra_columns = report.issues_by_type(DriftType.EXTRA_COLUMN)
        assert len(extra_columns) == 2

        extra_names = {issue.column_name for issue in extra_columns}
        assert "debug_info" in extra_names
        assert "internal_notes" in extra_names
