"""Tests for db_core.schema.sqlalchemy_gen — TableDef → SQLAlchemy Table converter."""
from __future__ import annotations

import pytest
from sqlalchemy import MetaData, BigInteger, Boolean, DateTime, Float, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID

from db_core.schema import ColumnDef, ForeignKeyDef, IndexDef, TableDef
from db_core.schema.sqlalchemy_gen import (
    _map_col_type,
    table_to_sqlalchemy,
    table_defs_to_metadata,
)


class TestMapColType:
    def test_bigint(self):
        t = _map_col_type("BIGINT")
        assert isinstance(t, BigInteger)

    def test_integer(self):
        t = _map_col_type("INTEGER")
        assert isinstance(t, Integer)

    def test_uuid(self):
        t = _map_col_type("UUID")
        assert isinstance(t, UUID)

    def test_text(self):
        t = _map_col_type("TEXT")
        assert isinstance(t, Text)

    def test_boolean(self):
        t = _map_col_type("BOOLEAN")
        assert isinstance(t, Boolean)

    def test_double_precision(self):
        t = _map_col_type("DOUBLE PRECISION")
        assert isinstance(t, Float)

    def test_timestamptz(self):
        t = _map_col_type("TIMESTAMPTZ")
        assert isinstance(t, DateTime)
        assert t.timezone is True

    def test_jsonb(self):
        t = _map_col_type("JSONB")
        assert isinstance(t, JSONB)

    def test_varchar(self):
        t = _map_col_type("VARCHAR(255)")
        assert isinstance(t, String)
        assert t.length == 255

    def test_varchar_case_insensitive(self):
        t = _map_col_type("varchar(64)")
        assert isinstance(t, String)
        assert t.length == 64

    def test_unknown_raises(self):
        with pytest.raises(ValueError, match="Unknown col_type"):
            _map_col_type("FANTASY_TYPE")


class TestTableToSqlalchemy:
    def test_basic_table_creation(self):
        td = TableDef(
            name="test_basic",
            columns=[
                ColumnDef("id", "UUID", primary_key=True),
                ColumnDef("name", "TEXT", nullable=False),
            ],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        assert t.name == "test_basic"
        assert "test_basic" in metadata.tables
        assert len(t.columns) == 2

        id_col = t.columns["id"]
        assert id_col.primary_key is True

        name_col = t.columns["name"]
        assert name_col.nullable is False

    def test_all_types(self):
        td = TableDef(
            name="test_types",
            columns=[
                ColumnDef("col_bigint", "BIGINT"),
                ColumnDef("col_integer", "INTEGER"),
                ColumnDef("col_uuid", "UUID"),
                ColumnDef("col_text", "TEXT"),
                ColumnDef("col_bool", "BOOLEAN"),
                ColumnDef("col_float", "DOUBLE PRECISION"),
                ColumnDef("col_ts", "TIMESTAMPTZ"),
                ColumnDef("col_jsonb", "JSONB"),
                ColumnDef("col_varchar", "VARCHAR(128)"),
            ],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        assert len(t.columns) == 9
        type_map = {
            "col_bigint": BigInteger,
            "col_integer": Integer,
            "col_uuid": UUID,
            "col_text": Text,
            "col_bool": Boolean,
            "col_float": Float,
            "col_ts": DateTime,
            "col_jsonb": JSONB,
            "col_varchar": String,
        }
        for name, expected_type in type_map.items():
            assert isinstance(t.columns[name].type, expected_type), f"{name} type mismatch"

    def test_index_created(self):
        td = TableDef(
            name="test_idx",
            columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("email", "TEXT")],
            indexes=[IndexDef(["email"])],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        assert len(t.indexes) == 1
        idx = next(iter(t.indexes))
        assert idx.name == "ix_test_idx_email"
        assert idx.unique is False

    def test_unique_index_created(self):
        td = TableDef(
            name="test_uq",
            columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("code", "TEXT")],
            indexes=[IndexDef(["code"], unique=True)],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        idx = next(iter(t.indexes))
        assert idx.name == "uq_test_uq_code"
        assert idx.unique is True

    def test_gin_index_created(self):
        td = TableDef(
            name="test_gin",
            columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("meta", "JSONB")],
            indexes=[IndexDef(["meta"], using="gin")],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        idx = next(iter(t.indexes))
        assert idx.dialect_kwargs.get("postgresql_using") == "gin"

    def test_explicit_index_name(self):
        td = TableDef(
            name="test_named",
            columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("slug", "TEXT")],
            indexes=[IndexDef(["slug"], name="my_custom_idx")],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        # Iterate indexes to find the named one
        idx_names = {i.name for i in t.indexes}
        assert "my_custom_idx" in idx_names

    def test_foreign_key_created(self):
        td = TableDef(
            name="test_fk",
            columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("parent_id", "INTEGER")],
            foreign_keys=[ForeignKeyDef("parent_id", "parents")],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata)

        assert len(t.foreign_key_constraints) == 1
        fk = list(t.foreign_key_constraints)[0]
        assert fk.name == "fk_test_fk_parent_id_parents"

    def test_db_include_filters_columns(self):
        td = TableDef(
            name="test_filtered",
            columns=[
                ColumnDef("id", "INTEGER", primary_key=True),
                ColumnDef("title", "TEXT"),
                ColumnDef("body", "TEXT"),
                ColumnDef("score", "DOUBLE PRECISION"),
            ],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata, db_include={"id", "title"})

        col_names = [c.name for c in t.columns]
        assert "id" in col_names
        assert "title" in col_names
        assert "body" not in col_names
        assert "score" not in col_names

    def test_db_include_none_includes_all(self):
        td = TableDef(
            name="test_all",
            columns=[
                ColumnDef("id", "INTEGER", primary_key=True),
                ColumnDef("a", "TEXT"),
                ColumnDef("b", "TEXT"),
            ],
        )
        metadata = MetaData()
        t = table_to_sqlalchemy(td, metadata, db_include=None)

        assert len(t.columns) == 3


class TestTableDefsToMetadata:
    def test_multiple_tables(self):
        tds = [
            TableDef(name="t1", columns=[ColumnDef("id", "INTEGER", primary_key=True)]),
            TableDef(name="t2", columns=[ColumnDef("id", "INTEGER", primary_key=True), ColumnDef("val", "TEXT")]),
        ]
        metadata = table_defs_to_metadata(tds)

        assert "t1" in metadata.tables
        assert "t2" in metadata.tables
        assert len(metadata.tables["t1"].columns) == 1
        assert len(metadata.tables["t2"].columns) == 2

    def test_db_include_map(self):
        tds = [
            TableDef(
                name="filtered",
                columns=[
                    ColumnDef("id", "INTEGER", primary_key=True),
                    ColumnDef("secret", "TEXT"),
                    ColumnDef("public", "TEXT"),
                ],
            ),
        ]
        metadata = table_defs_to_metadata(tds, db_include_map={"filtered": {"id", "public"}})

        cols = [c.name for c in metadata.tables["filtered"].columns]
        assert "id" in cols
        assert "public" in cols
        assert "secret" not in cols
