"""Tests for bitable service layer (U2): upsert, field deletion, view filtering.

Requires PostgreSQL — marked ``postgres``.
"""

from __future__ import annotations

import pytest

from agentkit.bitable.models import FieldOwner, FieldType
from agentkit.bitable.service import FieldDependencyError

pytestmark = pytest.mark.postgres


# ---------------------------------------------------------------------------
# Upsert (KTD8: jsonb_set preserves user columns)
# ---------------------------------------------------------------------------


async def test_upsert_inserts_new_records(bitable_service) -> None:
    """First upsert inserts all records."""
    table = await bitable_service.create_table(name="T")
    pk_field = await bitable_service.create_field(
        table_id=table.id, name="id", field_type=FieldType.text, owner=FieldOwner.agent
    )
    data_field = await bitable_service.create_field(
        table_id=table.id, name="data", field_type=FieldType.text, owner=FieldOwner.agent
    )
    await bitable_service.update_table(table.id, primary_key_field_id=pk_field.id)

    result = await bitable_service.upsert_records(
        table.id,
        [
            {pk_field.id: "row1", data_field.id: "hello"},
            {pk_field.id: "row2", data_field.id: "world"},
        ],
        pk_field.id,
    )
    assert result == {"inserted": 2, "updated": 0, "skipped": 0}

    records, _ = await bitable_service.list_records(table.id)
    assert len(records) == 2


async def test_upsert_updates_existing_preserves_user_columns(bitable_service) -> None:
    """KTD8: upsert updates agent columns via jsonb_set, user columns untouched."""
    table = await bitable_service.create_table(name="T")
    pk_field = await bitable_service.create_field(
        table_id=table.id, name="id", field_type=FieldType.text, owner=FieldOwner.agent
    )
    agent_field = await bitable_service.create_field(
        table_id=table.id, name="agent_data", field_type=FieldType.text, owner=FieldOwner.agent
    )
    user_field = await bitable_service.create_field(
        table_id=table.id, name="user_data", field_type=FieldType.text, owner=FieldOwner.user
    )
    await bitable_service.update_table(table.id, primary_key_field_id=pk_field.id)

    # First: insert with both agent and user values
    await bitable_service.upsert_records(
        table.id,
        [{pk_field.id: "row1", agent_field.id: "agent_v1", user_field.id: "user_v1"}],
        pk_field.id,
    )

    # Manually set user column (simulating user edit)
    records, _ = await bitable_service.list_records(table.id)
    assert len(records) == 1
    rec = records[0]
    await bitable_service.update_record_values(rec.id, {**rec.values, user_field.id: "USER_EDITED"})

    # Second upsert: only agent column changes
    result = await bitable_service.upsert_records(
        table.id,
        [{pk_field.id: "row1", agent_field.id: "agent_v2", user_field.id: "SHOULD_NOT_APPLY"}],
        pk_field.id,
    )
    assert result == {"inserted": 0, "updated": 1, "skipped": 0}

    # Verify: agent column updated, user column preserved
    records, _ = await bitable_service.list_records(table.id)
    assert len(records) == 1
    rec = records[0]
    assert rec.values[agent_field.id] == "agent_v2"  # updated
    assert rec.values[user_field.id] == "USER_EDITED"  # preserved (NOT "SHOULD_NOT_APPLY")


async def test_upsert_skips_records_without_pk(bitable_service) -> None:
    """Records without PK value are skipped."""
    table = await bitable_service.create_table(name="T")
    pk_field = await bitable_service.create_field(
        table_id=table.id, name="id", field_type=FieldType.text, owner=FieldOwner.agent
    )
    await bitable_service.update_table(table.id, primary_key_field_id=pk_field.id)

    result = await bitable_service.upsert_records(
        table.id,
        [{pk_field.id: "row1"}, {}],  # second has no PK
        pk_field.id,
    )
    assert result == {"inserted": 1, "updated": 0, "skipped": 1}


async def test_upsert_empty_batch(bitable_service) -> None:
    """Empty batch returns all zeros."""
    table = await bitable_service.create_table(name="T")
    pk_field = await bitable_service.create_field(
        table_id=table.id, name="id", field_type=FieldType.text, owner=FieldOwner.agent
    )
    result = await bitable_service.upsert_records(table.id, [], pk_field.id)
    assert result == {"inserted": 0, "updated": 0, "skipped": 0}


async def test_upsert_without_pk_field_raises(bitable_service) -> None:
    """Upsert without primary_key_field_id raises ValueError."""
    table = await bitable_service.create_table(name="T")
    with pytest.raises(ValueError, match="primary_key_field_id"):
        await bitable_service.upsert_records(table.id, [{}], "")


# ---------------------------------------------------------------------------
# Field deletion with dependency checking
# ---------------------------------------------------------------------------


async def test_delete_field_no_dependencies(bitable_service) -> None:
    """Deleting a field with no dependencies succeeds."""
    table = await bitable_service.create_table(name="T")
    field = await bitable_service.create_field(
        table_id=table.id, name="f", field_type=FieldType.text
    )
    deleted = await bitable_service.delete_field(field.id)
    assert deleted is True
    assert await bitable_service.get_field(field.id) is None


async def test_delete_field_referenced_by_formula_returns_deps(bitable_service) -> None:
    """Deleting a field referenced by a formula raises FieldDependencyError."""
    table = await bitable_service.create_table(name="T")
    source_field = await bitable_service.create_field(
        table_id=table.id, name="source", field_type=FieldType.number
    )
    formula_field = await bitable_service.create_field(
        table_id=table.id,
        name="calc",
        field_type=FieldType.formula,
        config={"formula_expr": f"={source_field.id} * 2"},
    )

    with pytest.raises(FieldDependencyError) as exc_info:
        await bitable_service.delete_field(source_field.id)

    deps = exc_info.value.dependencies
    assert "formula_fields" in deps
    assert any(f["id"] == formula_field.id for f in deps["formula_fields"])


async def test_delete_primary_key_field_returns_deps(bitable_service) -> None:
    """Deleting the primary key field raises FieldDependencyError."""
    table = await bitable_service.create_table(name="T")
    pk_field = await bitable_service.create_field(
        table_id=table.id, name="id", field_type=FieldType.text
    )
    await bitable_service.update_table(table.id, primary_key_field_id=pk_field.id)

    with pytest.raises(FieldDependencyError) as exc_info:
        await bitable_service.delete_field(pk_field.id)

    assert exc_info.value.dependencies.get("is_primary_key") is True


async def test_delete_field_force_casces_cleanup(bitable_service) -> None:
    """Force delete cascades: removes field from records, marks formula as error."""
    table = await bitable_service.create_table(name="T")
    source_field = await bitable_service.create_field(
        table_id=table.id, name="source", field_type=FieldType.number, owner=FieldOwner.agent
    )
    formula_field = await bitable_service.create_field(
        table_id=table.id,
        name="calc",
        field_type=FieldType.formula,
        config={"formula_expr": f"={source_field.id} * 2"},
    )
    # Create a record with the source field value
    record = await bitable_service.create_record(table_id=table.id, values={source_field.id: 42})

    # Force delete
    deleted = await bitable_service.delete_field(source_field.id, force=True)
    assert deleted is True

    # Record should no longer have the source field key
    rec = await bitable_service.get_record(record.id)
    assert rec is not None
    assert source_field.id not in rec.values

    # Formula field should have error in config
    formula = await bitable_service.get_field(formula_field.id)
    assert formula is not None
    assert "error" in formula.config


# ---------------------------------------------------------------------------
# View-filtered record listing
# ---------------------------------------------------------------------------


async def test_list_records_filtered_by_number_gt(bitable_service) -> None:
    """View filter with gt op on number field correctly filters (CAST NUMERIC)."""
    table = await bitable_service.create_table(name="T")
    num_field = await bitable_service.create_field(
        table_id=table.id, name="amount", field_type=FieldType.number, owner=FieldOwner.agent
    )

    # Create records with various amounts
    for amt in [10, 50, 100, 200]:
        await bitable_service.create_record(table_id=table.id, values={num_field.id: amt})

    # Filter: amount > 50
    records, _ = await bitable_service.list_records_filtered(
        table.id,
        filters=[{"field_id": num_field.id, "op": "gt", "value": 50}],
    )
    amounts = [r.values[num_field.id] for r in records]
    assert all(a > 50 for a in amounts)
    assert len(records) == 2  # 100 and 200


async def test_list_records_filtered_by_text_eq(bitable_service) -> None:
    """View filter with eq op on text field."""
    table = await bitable_service.create_table(name="T")
    text_field = await bitable_service.create_field(
        table_id=table.id, name="status", field_type=FieldType.text, owner=FieldOwner.agent
    )

    for status in ["open", "closed", "open", "pending"]:
        await bitable_service.create_record(table_id=table.id, values={text_field.id: status})

    records, _ = await bitable_service.list_records_filtered(
        table.id,
        filters=[{"field_id": text_field.id, "op": "eq", "value": "open"}],
    )
    assert len(records) == 2
    assert all(r.values[text_field.id] == "open" for r in records)


async def test_list_records_filtered_with_sort(bitable_service) -> None:
    """View sort by number field descending."""
    table = await bitable_service.create_table(name="T")
    num_field = await bitable_service.create_field(
        table_id=table.id, name="score", field_type=FieldType.number, owner=FieldOwner.agent
    )

    for score in [30, 10, 50, 20]:
        await bitable_service.create_record(table_id=table.id, values={num_field.id: score})

    records, _ = await bitable_service.list_records_filtered(
        table.id,
        sorts=[{"field_id": num_field.id, "direction": "desc"}],
    )
    # Records should be sorted by score descending (as text, but single/double digit sorts OK)
    assert len(records) == 4


async def test_list_records_filtered_cursor_pagination(bitable_service) -> None:
    """Cursor pagination with filters."""
    table = await bitable_service.create_table(name="T")
    text_field = await bitable_service.create_field(
        table_id=table.id, name="name", field_type=FieldType.text, owner=FieldOwner.agent
    )

    for i in range(5):
        await bitable_service.create_record(table_id=table.id, values={text_field.id: f"item_{i}"})

    # First page
    records, next_cursor = await bitable_service.list_records_filtered(table.id, limit=2)
    assert len(records) == 2
    assert next_cursor is not None

    # Second page
    records2, next_cursor2 = await bitable_service.list_records_filtered(
        table.id, cursor=next_cursor, limit=2
    )
    assert len(records2) == 2
    assert next_cursor2 is not None

    # Third page
    records3, next_cursor3 = await bitable_service.list_records_filtered(
        table.id, cursor=next_cursor2, limit=2
    )
    assert len(records3) == 1
    assert next_cursor3 is None

    # All records unique
    all_ids = {r.id for r in [records, records2, records3] for r in r}
    assert len(all_ids) == 5
