"""Tests for bitable DB initialization, schema, and constraints (U1).

PG-dependent tests skip via the ``bitable_db`` fixture when PostgreSQL is
unavailable. The no-URL degradation test runs in all environments.
"""

from __future__ import annotations

import pytest


# ---------------------------------------------------------------------------
# init_bitable_db / BitableDB.init
# ---------------------------------------------------------------------------


async def test_init_creates_schema_and_all_tables(bitable_db) -> None:
    """init creates the bitable schema and all 7 tables (V2 adds bitable_files)."""
    from sqlalchemy import text

    async with bitable_db.engine.begin() as conn:
        # Schema exists
        result = await conn.execute(
            text(
                "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'bitable'"
            )
        )
        assert result.fetchone() is not None

        # All 7 tables present (V2 adds bitable_files)
        result = await conn.execute(
            text(
                "SELECT table_name FROM information_schema.tables "
                "WHERE table_schema = 'bitable' ORDER BY table_name"
            )
        )
        tables = {row[0] for row in result.fetchall()}
        assert tables == {
            "bitable_fields",
            "bitable_files",
            "bitable_meta",
            "bitable_records",
            "bitable_recalc_queue",
            "bitable_tables",
            "bitable_views",
        }


async def test_init_is_idempotent(bitable_db) -> None:
    """Calling init() twice does not raise and keeps schema intact."""
    # bitable_db fixture already called init(); call again
    await bitable_db.init()
    await bitable_db.init()  # third time also fine

    from sqlalchemy import text

    async with bitable_db.engine.begin() as conn:
        result = await conn.execute(text("SELECT COUNT(*) FROM bitable.bitable_meta"))
        assert result.fetchone()[0] >= 1


async def test_schema_version_recorded_in_meta(bitable_db) -> None:
    """bitable_meta stores the current schema version."""
    from agentkit.bitable.db import _META_SCHEMA_VERSION_KEY, _SCHEMA_VERSION

    from sqlalchemy import text

    async with bitable_db.engine.begin() as conn:
        result = await conn.execute(
            text("SELECT value FROM bitable.bitable_meta WHERE key = :key"),
            {"key": _META_SCHEMA_VERSION_KEY},
        )
        row = result.fetchone()
        assert row is not None
        assert int(row[0]) == _SCHEMA_VERSION


# ---------------------------------------------------------------------------
# Constraints
# ---------------------------------------------------------------------------


async def test_recalc_queue_unique_record_field(bitable_db) -> None:
    """Recalc queue enforces (record_id, field_id) uniqueness — dedup."""
    from agentkit.bitable.models import FieldType
    from agentkit.bitable.repository import BitableRepository

    repo = BitableRepository(bitable_db)
    table = await repo.create_table(name="T")
    field = await repo.create_field(table_id=table.id, name="f", field_type=FieldType.text)
    record = await repo.create_record(table_id=table.id)

    # First enqueue succeeds
    task1 = await repo.enqueue_recalc(table.id, record.id, field.id)
    assert task1 is not None

    # Second enqueue is a no-op (ON CONFLICT DO NOTHING) — returns None
    task2 = await repo.enqueue_recalc(table.id, record.id, field.id)
    assert task2 is None


async def test_recalc_queue_status_index_exists(bitable_db) -> None:
    """The (status, queued_at) index exists for worker consumption."""
    from sqlalchemy import text

    async with bitable_db.engine.begin() as conn:
        result = await conn.execute(
            text(
                "SELECT indexname FROM pg_indexes "
                "WHERE schemaname = 'bitable' AND tablename = 'bitable_recalc_queue'"
            )
        )
        indexes = {row[0] for row in result.fetchall()}
        assert "ix_recalc_status_queued" in indexes
        assert "uq_recalc_record_field" in indexes


async def test_records_values_gin_index_exists(bitable_db) -> None:
    """GIN index on records.values exists for JSONB key lookups."""
    from sqlalchemy import text

    async with bitable_db.engine.begin() as conn:
        result = await conn.execute(
            text(
                "SELECT indexname FROM pg_indexes "
                "WHERE schemaname = 'bitable' AND tablename = 'bitable_records'"
            )
        )
        indexes = {row[0] for row in result.fetchall()}
        assert "ix_bitable_records_values_gin" in indexes


# ---------------------------------------------------------------------------
# Repository CRUD smoke (verifies schema is usable end-to-end)
# ---------------------------------------------------------------------------


async def test_repository_crud_round_trip(bitable_db) -> None:
    """Repository can create/get/list/delete across all entities."""
    from agentkit.bitable.models import FieldOwner, FieldType, ViewType
    from agentkit.bitable.repository import BitableRepository

    repo = BitableRepository(bitable_db)

    # Table
    table = await repo.create_table(name="Orders", description="desc")
    assert table.name == "Orders"
    fetched = await repo.get_table(table.id)
    assert fetched is not None and fetched.id == table.id

    # Field
    field = await repo.create_field(
        table_id=table.id,
        name="Amount",
        field_type=FieldType.number,
        owner=FieldOwner.agent,
    )
    fields = await repo.list_fields(table.id)
    assert len(fields) == 1
    assert fields[0].id == field.id

    # Record
    record = await repo.create_record(table_id=table.id, values={field.id: 42})
    fetched_rec = await repo.get_record(record.id)
    assert fetched_rec is not None
    assert fetched_rec.values[field.id] == 42

    # Cursor pagination
    rec2 = await repo.create_record(table_id=table.id, values={field.id: 99})
    records, next_cursor = await repo.list_records(table.id, limit=1)
    assert len(records) == 1
    assert next_cursor is not None
    records2, next_cursor2 = await repo.list_records(table.id, cursor=next_cursor, limit=1)
    assert len(records2) == 1
    # The second page should be the other record
    assert {records[0].id, records2[0].id} == {record.id, rec2.id}

    # View
    view = await repo.create_view(table_id=table.id, name="All", view_type=ViewType.grid)
    views = await repo.list_views(table.id)
    assert len(views) == 1 and views[0].id == view.id

    # Delete cascades
    deleted = await repo.delete_table(table.id)
    assert deleted is True
    assert await repo.get_table(table.id) is None
    assert await repo.get_field(field.id) is None
    assert await repo.get_record(record.id) is None
    assert (await repo.list_views(table.id)) == []


# ---------------------------------------------------------------------------
# Crash recovery
# ---------------------------------------------------------------------------


async def test_reset_stale_recalc_tasks(bitable_db) -> None:
    """reset_stale_recalc_tasks flips 'calculating' back to 'pending'."""
    from agentkit.bitable.models import FieldType, RecalcStatus
    from agentkit.bitable.repository import BitableRepository

    repo = BitableRepository(bitable_db)
    table = await repo.create_table(name="T")
    field = await repo.create_field(table_id=table.id, name="f", field_type=FieldType.text)
    record = await repo.create_record(table_id=table.id)

    task = await repo.enqueue_recalc(table.id, record.id, field.id)
    assert task is not None

    # Simulate a worker crash mid-calculation
    await repo.update_recalc_status(task.id, RecalcStatus.calculating)

    reset_count = await repo.reset_stale_recalc_tasks()
    assert reset_count == 1

    pending = await repo.get_pending_recalc_tasks()
    assert any(t.id == task.id for t in pending)


# ---------------------------------------------------------------------------
# Degradation (no PG)
# ---------------------------------------------------------------------------


async def test_bitable_db_without_url_raises() -> None:
    """BitableDB with no URL raises RuntimeError on init (not silently None)."""
    # Clear env vars for this test to ensure no URL resolution
    import os

    saved = (
        os.environ.pop("DATABASE_URL", None),
        os.environ.pop("AGENTKIT_DATABASE_URL", None),
    )
    try:
        from agentkit.bitable.db import BitableDB

        db = BitableDB(database_url=None)
        # _database_url is None because no arg and no env
        assert db.database_url is None
        with pytest.raises(RuntimeError, match="No database URL"):
            await db.init()
    finally:
        for key, val in zip(("DATABASE_URL", "AGENTKIT_DATABASE_URL"), saved):
            if val is not None:
                os.environ[key] = val
