"""Tests for bitable Pydantic v2 data models (U1).

Covers: enum values, round-trip serialization, field config shapes per
field_type, Record.values empty-dict legality, default factories.
"""

from __future__ import annotations

from datetime import datetime, timezone

import pytest
from pydantic import ValidationError

from agentkit.bitable.models import (
    Field,
    FieldOwner,
    FieldType,
    Record,
    RecalcStatus,
    RecalcTask,
    Table,
    View,
    ViewType,
)


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


def test_field_type_values() -> None:
    """FieldType has the 9 supported types with correct string values."""
    expected = {
        "text",
        "number",
        "date",
        "select",
        "multiselect",
        "attachment",
        "image",
        "formula",
        "lookup",
    }
    assert {ft.value for ft in FieldType} == expected


def test_field_owner_values() -> None:
    """FieldOwner distinguishes agent vs user (drives upsert merge)."""
    assert FieldOwner.agent.value == "agent"
    assert FieldOwner.user.value == "user"


def test_view_type_values() -> None:
    """ViewType enumerates the 5 view kinds (v1 only grid is implemented)."""
    assert {vt.value for vt in ViewType} == {"grid", "kanban", "gantt", "gallery", "form"}


def test_recalc_status_lifecycle() -> None:
    """RecalcStatus covers the full recalc lifecycle."""
    assert {rs.value for rs in RecalcStatus} == {"pending", "calculating", "done", "error"}


# ---------------------------------------------------------------------------
# Table
# ---------------------------------------------------------------------------


def test_table_minimal_construction() -> None:
    """Table requires id + name; other fields have defaults."""
    table = Table(id="t1", name="Orders")
    assert table.id == "t1"
    assert table.name == "Orders"
    assert table.description == ""
    assert table.primary_key_field_id is None
    assert table.owner_user_id is None
    assert isinstance(table.created_at, datetime)
    assert isinstance(table.updated_at, datetime)


def test_table_round_trip() -> None:
    """Table serializes to dict and re-parses losslessly."""
    table = Table(
        id="t1",
        name="Orders",
        description="Customer orders",
        primary_key_field_id="f_pk",
        owner_user_id="u1",
    )
    data = table.model_dump(mode="json")
    restored = Table.model_validate(data)
    assert restored == table


def test_table_requires_id_and_name() -> None:
    """Table requires id and name (non-optional)."""
    with pytest.raises(ValidationError):
        Table(name="no id")  # type: ignore[call-arg]
    with pytest.raises(ValidationError):
        Table(id="t1")  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# Field
# ---------------------------------------------------------------------------


def test_field_text_default_config() -> None:
    """Text field has empty config by default and user owner."""
    field = Field(id="f1", table_id="t1", name="Title", field_type=FieldType.text)
    assert field.config == {}
    assert field.owner == FieldOwner.user


def test_field_select_config_shape() -> None:
    """Select field config carries options list."""
    field = Field(
        id="f1",
        table_id="t1",
        name="Status",
        field_type=FieldType.select,
        config={"options": [{"label": "Open", "value": "open"}]},
        owner=FieldOwner.agent,
    )
    assert field.config["options"][0]["value"] == "open"
    assert field.owner == FieldOwner.agent


def test_field_formula_config_shape() -> None:
    """Formula field config carries formula_expr."""
    field = Field(
        id="f1",
        table_id="t1",
        name="Total",
        field_type=FieldType.formula,
        config={"formula_expr": "=SUM({f_price})"},
    )
    assert field.config["formula_expr"] == "=SUM({f_price})"


def test_field_lookup_config_shape() -> None:
    """Lookup field config carries lookup_target with table/field ids."""
    field = Field(
        id="f1",
        table_id="t1",
        name="Customer Name",
        field_type=FieldType.lookup,
        config={
            "lookup_target": {
                "table_id": "t_customers",
                "field_id": "f_name",
                "filter_field_id": "f_id",
                "filter_value": "cust-123",
            }
        },
    )
    assert field.config["lookup_target"]["field_id"] == "f_name"


def test_field_round_trip() -> None:
    """Field serializes and re-parses losslessly across types."""
    field = Field(
        id="f1",
        table_id="t1",
        name="Score",
        field_type=FieldType.number,
        config={"precision": 2},
        owner=FieldOwner.agent,
    )
    restored = Field.model_validate(field.model_dump(mode="json"))
    assert restored == field
    assert restored.field_type == FieldType.number
    assert restored.owner == FieldOwner.agent


def test_field_type_accepts_string() -> None:
    """FieldType coerces from string (JSON round-trip scenario)."""
    field = Field(id="f1", table_id="t1", name="X", field_type="number")  # type: ignore[arg-type]
    assert field.field_type == FieldType.number


# ---------------------------------------------------------------------------
# Record
# ---------------------------------------------------------------------------


def test_record_empty_values_allowed() -> None:
    """Record.values defaults to empty dict (new row before data entry)."""
    record = Record(id="r1", table_id="t1")
    assert record.values == {}


def test_record_values_round_trip() -> None:
    """Record.values (JSONB-shaped dict) round-trips through JSON."""
    record = Record(
        id="r1",
        table_id="t1",
        values={"f_name": "Alice", "f_age": 30, "f_tags": ["a", "b"]},
    )
    restored = Record.model_validate(record.model_dump(mode="json"))
    assert restored.values == record.values
    assert restored.values["f_tags"] == ["a", "b"]


def test_record_values_with_null() -> None:
    """Record.values can carry None for unset fields."""
    record = Record(id="r1", table_id="t1", values={"f_name": None})
    assert record.values["f_name"] is None


# ---------------------------------------------------------------------------
# View
# ---------------------------------------------------------------------------


def test_view_defaults_to_grid() -> None:
    """View defaults to grid type with empty config."""
    view = View(id="v1", table_id="t1", name="All")
    assert view.view_type == ViewType.grid
    assert view.config == {}


def test_view_round_trip() -> None:
    """View with filter/sort config round-trips."""
    view = View(
        id="v1",
        table_id="t1",
        name="Open only",
        view_type=ViewType.grid,
        config={
            "filters": [{"field_id": "f_status", "op": "eq", "value": "open"}],
            "sorts": [{"field_id": "f_created", "direction": "desc"}],
            "hidden_fields": ["f_internal"],
        },
    )
    restored = View.model_validate(view.model_dump(mode="json"))
    assert restored == view
    assert restored.config["filters"][0]["op"] == "eq"


# ---------------------------------------------------------------------------
# RecalcTask
# ---------------------------------------------------------------------------


def test_recalc_task_defaults() -> None:
    """RecalcTask defaults to pending status, no error, no completed_at."""
    task = RecalcTask(id="q1", table_id="t1", record_id="r1", field_id="f1")
    assert task.status == RecalcStatus.pending
    assert task.error_message is None
    assert task.completed_at is None
    assert isinstance(task.queued_at, datetime)


def test_recalc_task_error_state() -> None:
    """RecalcTask in error state carries message and completed_at."""
    task = RecalcTask(
        id="q1",
        table_id="t1",
        record_id="r1",
        field_id="f1",
        status=RecalcStatus.error,
        error_message="division by zero",
        completed_at=datetime.now(timezone.utc),
    )
    assert task.status == RecalcStatus.error
    assert task.error_message == "division by zero"


def test_recalc_task_round_trip() -> None:
    """RecalcTask round-trips through JSON."""
    task = RecalcTask(
        id="q1",
        table_id="t1",
        record_id="r1",
        field_id="f1",
        status=RecalcStatus.done,
    )
    restored = RecalcTask.model_validate(task.model_dump(mode="json"))
    assert restored == task
    assert restored.status == RecalcStatus.done


# ---------------------------------------------------------------------------
# from_attributes (ORM row compatibility)
# ---------------------------------------------------------------------------


def test_table_from_attributes() -> None:
    """Table.model_validate accepts an ORM-like object (from_attributes)."""

    class _Row:
        id = "t1"
        name = "Orders"
        description = "desc"
        primary_key_field_id = None
        owner_user_id = None
        created_at = datetime.now(timezone.utc)
        updated_at = datetime.now(timezone.utc)

    table = Table.model_validate(_Row())
    assert table.id == "t1"
    assert table.name == "Orders"
