"""Tests for zuzu.record — data layer."""

import os
import tempfile
import pytest
import zuzu
from zuzu.record import set_db_path, _registry


@pytest.fixture(autouse=True)
def fresh_db(tmp_path):
    """Use a fresh database for each test."""
    db_path = tmp_path / "test.db"
    set_db_path(db_path)
    _registry.clear()
    yield db_path


def test_record_creation():
    clients = zuzu.record("Clients", name="", email="", revenue=0.0)
    assert clients._name == "Clients"
    assert clients._table_name == "clients"
    assert "name" in clients._fields
    assert "email" in clients._fields
    assert "revenue" in clients._fields


def test_record_type_inference():
    r = zuzu.record("Test",
        text_field="",
        int_field=0,
        float_field=0.0,
        bool_field=True,
        choice_field=["a", "b", "c"],
    )
    assert r._field_types["text_field"] == "TEXT"
    assert r._field_types["int_field"] == "INTEGER"
    assert r._field_types["float_field"] == "REAL"
    assert r._field_types["bool_field"] == "INTEGER"
    assert r._field_types["choice_field"] == "TEXT"


def test_add_and_get():
    clients = zuzu.record("Clients", name="", email="")
    result = clients.add(name="Alice", email="alice@co.com")
    assert result["name"] == "Alice"
    assert result["email"] == "alice@co.com"
    assert result["id"] is not None  # UUID string
    assert isinstance(result["id"], str)
    assert len(result["id"]) > 10


def test_count():
    clients = zuzu.record("Clients", name="")
    assert clients.count() == 0
    clients.add(name="Alice")
    clients.add(name="Bob")
    assert clients.count() == 2


def test_all():
    clients = zuzu.record("Clients", name="", score=0)
    clients.add(name="Alice", score=90)
    clients.add(name="Bob", score=80)
    all_clients = clients.all()
    assert len(all_clients) == 2
    assert all_clients[0]["name"] == "Alice"


def test_update():
    clients = zuzu.record("Clients", name="", score=0)
    result = clients.add(name="Alice", score=80)
    rid = result["id"]
    clients.update(rid, score=95)
    updated = clients.get(id=rid)
    assert updated["score"] == 95


def test_remove():
    clients = zuzu.record("Clients", name="")
    result = clients.add(name="Alice")
    assert clients.count() == 1
    clients.remove(result["id"])
    assert clients.count() == 0


def test_total_and_average():
    clients = zuzu.record("Clients", name="", score=0.0)
    clients.add(name="Alice", score=90.0)
    clients.add(name="Bob", score=80.0)
    assert clients.total("score") == 170.0
    assert clients.average("score") == 85.0


def test_where_keyword():
    clients = zuzu.record("Clients", name="", status="")
    clients.add(name="Alice", status="active")
    clients.add(name="Bob", status="churned")
    clients.add(name="Charlie", status="active")

    active = clients.where(status="active").all()
    assert len(active) == 2


def test_where_operator_gt():
    clients = zuzu.record("Clients", name="", revenue=0.0)
    clients.add(name="Alice", revenue=100.0)
    clients.add(name="Bob", revenue=50.0)
    clients.add(name="Charlie", revenue=200.0)

    big = clients.where(clients.revenue > 75).all()
    assert len(big) == 2
    names = {r["name"] for r in big}
    assert names == {"Alice", "Charlie"}


def test_where_operator_eq():
    clients = zuzu.record("Clients", name="", status="")
    clients.add(name="Alice", status="active")
    clients.add(name="Bob", status="churned")

    active = clients.where(clients.status == "active").all()
    assert len(active) == 1
    assert active[0]["name"] == "Alice"


def test_where_compound_and():
    clients = zuzu.record("Clients", name="", status="", revenue=0.0)
    clients.add(name="Alice", status="active", revenue=100.0)
    clients.add(name="Bob", status="active", revenue=30.0)
    clients.add(name="Charlie", status="churned", revenue=200.0)

    result = clients.where(
        (clients.status == "active") & (clients.revenue > 50)
    ).all()
    assert len(result) == 1
    assert result[0]["name"] == "Alice"


def test_where_contains():
    clients = zuzu.record("Clients", name="")
    clients.add(name="TechCorp")
    clients.add(name="BioLabs")
    clients.add(name="TechStart")

    result = clients.where(clients.name.contains("Tech")).all()
    assert len(result) == 2


def test_where_is_in():
    clients = zuzu.record("Clients", name="", status="")
    clients.add(name="Alice", status="lead")
    clients.add(name="Bob", status="active")
    clients.add(name="Charlie", status="churned")

    result = clients.where(clients.status.is_in(["lead", "active"])).all()
    assert len(result) == 2


def test_sort():
    clients = zuzu.record("Clients", name="", revenue=0.0)
    clients.add(name="Alice", revenue=100.0)
    clients.add(name="Bob", revenue=300.0)
    clients.add(name="Charlie", revenue=200.0)

    result = clients.sort("revenue", descending=True).all()
    assert result[0]["name"] == "Bob"
    assert result[1]["name"] == "Charlie"
    assert result[2]["name"] == "Alice"


def test_first():
    clients = zuzu.record("Clients", name="")
    for i in range(10):
        clients.add(name=f"Client {i}")

    result = clients.first(3).all()
    assert len(result) == 3


def test_chaining():
    clients = zuzu.record("Clients", name="", status="", revenue=0.0)
    clients.add(name="Alice", status="active", revenue=100.0)
    clients.add(name="Bob", status="active", revenue=300.0)
    clients.add(name="Charlie", status="churned", revenue=200.0)

    result = (
        clients
        .where(clients.status == "active")
        .sort("revenue", descending=True)
        .first(1)
        .all()
    )
    assert len(result) == 1
    assert result[0]["name"] == "Bob"


def test_filtered_count():
    clients = zuzu.record("Clients", name="", status="")
    clients.add(name="Alice", status="active")
    clients.add(name="Bob", status="churned")
    clients.add(name="Charlie", status="active")

    assert clients.where(clients.status == "active").count() == 2
    assert clients.where(clients.status == "churned").count() == 1


def test_filtered_total():
    clients = zuzu.record("Clients", name="", status="", revenue=0.0)
    clients.add(name="Alice", status="active", revenue=100.0)
    clients.add(name="Bob", status="churned", revenue=50.0)

    assert clients.where(clients.status == "active").total("revenue") == 100.0


# ── Feature 6: Computed fields ──────────────────────────────────────────

def test_computed_fields_basic():
    """Computed fields should appear in returned rows but not in the DB."""
    clients = zuzu.record("Clients",
        name="",
        revenue=0.0,
        costs=0.0,
        profit=lambda r: r["revenue"] - r["costs"],
    )
    result = clients.add(name="Alice", revenue=100.0, costs=30.0)
    assert result["profit"] == 70.0

    # Verify computed field is not a DB column
    assert "profit" not in clients._fields
    assert "profit" not in clients._field_types


def test_computed_fields_in_all():
    clients = zuzu.record("Clients",
        name="",
        revenue=0.0,
        tier=lambda r: "gold" if r["revenue"] > 100 else "silver",
    )
    clients.add(name="Alice", revenue=200.0)
    clients.add(name="Bob", revenue=50.0)

    rows = clients.all()
    assert rows[0]["tier"] == "gold"
    assert rows[1]["tier"] == "silver"


def test_computed_fields_in_get():
    clients = zuzu.record("Clients",
        name="",
        revenue=0.0,
        tier=lambda r: "gold" if r["revenue"] > 100 else "silver",
    )
    result = clients.add(name="Alice", revenue=200.0)
    fetched = clients.get(id=result["id"])
    assert fetched["tier"] == "gold"


def test_computed_fields_in_query():
    clients = zuzu.record("Clients",
        name="",
        revenue=0.0,
        tier=lambda r: "gold" if r["revenue"] > 100 else "silver",
    )
    clients.add(name="Alice", revenue=200.0)
    clients.add(name="Bob", revenue=50.0)

    rows = clients.where(clients.revenue > 0).all()
    tiers = {r["name"]: r["tier"] for r in rows}
    assert tiers["Alice"] == "gold"
    assert tiers["Bob"] == "silver"


def test_computed_field_error_returns_none():
    """Computed fields that raise exceptions should return None."""
    clients = zuzu.record("Clients",
        name="",
        broken=lambda r: 1 / 0,
    )
    result = clients.add(name="Alice")
    assert result["broken"] is None


def test_computed_field_in_spec():
    clients = zuzu.record("Clients",
        name="",
        revenue=0.0,
        profit=lambda r: r["revenue"],
    )
    spec = clients._to_spec()
    assert "profit" in spec["fields"]
    assert spec["fields"]["profit"]["type"] == "computed"


# ── Feature 7: Choice field with colors ─────────────────────────────────

def test_choice_class():
    from zuzu import Choice
    clients = zuzu.record("Clients",
        name="",
        status=Choice(["lead", "active", "churned"], colors={
            "lead": "yellow",
            "active": "green",
            "churned": "red",
        }),
    )
    assert clients._choices["status"] == ["lead", "active", "churned"]
    assert clients._choice_colors["status"]["active"] == "green"
    assert clients._field_labels["status"] == "choice"
    assert clients._field_types["status"] == "TEXT"


def test_choice_in_spec():
    from zuzu import Choice
    clients = zuzu.record("Clients",
        name="",
        status=Choice(["lead", "active"], colors={"lead": "yellow", "active": "green"}),
    )
    spec = clients._to_spec()
    assert spec["fields"]["status"]["choices"] == ["lead", "active"]
    assert spec["fields"]["status"]["colors"] == {"lead": "yellow", "active": "green"}


def test_plain_list_still_works():
    """Plain list syntax should still work as before."""
    clients = zuzu.record("Clients",
        name="",
        status=["lead", "active", "churned"],
    )
    assert clients._choices["status"] == ["lead", "active", "churned"]
    assert "status" not in clients._choice_colors


def test_choice_without_colors():
    from zuzu import Choice
    clients = zuzu.record("Clients",
        name="",
        status=Choice(["a", "b"]),
    )
    assert clients._choices["status"] == ["a", "b"]
    assert "status" not in clients._choice_colors


# ── Feature 8: Number formatting types ──────────────────────────────────

def test_currency_field():
    from zuzu import Currency
    products = zuzu.record("Products",
        name="",
        price=Currency(0.0, symbol="$"),
    )
    assert products._field_types["price"] == "REAL"
    assert products._field_labels["price"] == "number"
    assert products._field_formats["price"] == {"format": "currency", "symbol": "$"}

    result = products.add(name="Widget", price=9.99)
    assert result["price"] == 9.99


def test_percent_field():
    from zuzu import Percent
    products = zuzu.record("Products",
        name="",
        tax_rate=Percent(0.0),
    )
    assert products._field_types["tax_rate"] == "REAL"
    assert products._field_formats["tax_rate"] == {"format": "percent"}


def test_rating_field():
    from zuzu import Rating
    products = zuzu.record("Products",
        name="",
        stars=Rating(0, max=5),
    )
    assert products._field_types["stars"] == "INTEGER"
    assert products._field_formats["stars"] == {"format": "rating", "max": 5}

    result = products.add(name="Widget", stars=4)
    assert result["stars"] == 4


def test_format_types_in_spec():
    from zuzu import Currency, Percent, Rating
    products = zuzu.record("Products",
        name="",
        price=Currency(0.0, symbol="EUR"),
        margin=Percent(0.0),
        stars=Rating(0, max=10),
    )
    spec = products._to_spec()
    assert spec["fields"]["price"]["format"] == "currency"
    assert spec["fields"]["price"]["symbol"] == "EUR"
    assert spec["fields"]["margin"]["format"] == "percent"
    assert spec["fields"]["stars"]["format"] == "rating"
    assert spec["fields"]["stars"]["max"] == 10


# ── Feature 9: CSV import / export ─────────────────────────────────────

def test_import_csv(tmp_path):
    csv_path = tmp_path / "data.csv"
    csv_path.write_text("name,amount,category\nLunch,12.50,food\nUber,8.00,transport\n")

    expenses = zuzu.record("Expenses", name="", amount=0.0, category="")
    count = expenses.import_csv(str(csv_path))
    assert count == 2

    rows = expenses.all()
    assert len(rows) == 2
    assert rows[0]["name"] == "Lunch"
    assert rows[0]["amount"] == 12.50
    assert rows[1]["category"] == "transport"


def test_import_csv_type_coercion(tmp_path):
    csv_path = tmp_path / "data.csv"
    csv_path.write_text("name,score\nAlice,95\nBob,\n")

    records = zuzu.record("Scores", name="", score=0)
    count = records.import_csv(str(csv_path))
    assert count == 2

    rows = records.all()
    assert rows[0]["score"] == 95
    assert rows[1]["score"] == 0  # empty value coerced to 0


def test_import_csv_ignores_extra_columns(tmp_path):
    csv_path = tmp_path / "data.csv"
    csv_path.write_text("name,extra_col,score\nAlice,ignore,90\n")

    records = zuzu.record("Scores", name="", score=0)
    count = records.import_csv(str(csv_path))
    assert count == 1
    row = records.all()[0]
    assert row["name"] == "Alice"
    assert row["score"] == 90


def test_export_csv(tmp_path):
    csv_path = tmp_path / "output.csv"
    expenses = zuzu.record("Expenses", name="", amount=0.0)
    expenses.add(name="Lunch", amount=12.50)
    expenses.add(name="Uber", amount=8.00)

    count = expenses.export_csv(str(csv_path))
    assert count == 2

    content = csv_path.read_text()
    lines = content.strip().split("\n")
    assert len(lines) == 3  # header + 2 rows
    assert "name" in lines[0]
    assert "amount" in lines[0]
    assert "created_at" not in lines[0]
    assert "updated_at" not in lines[0]


def test_export_csv_empty(tmp_path):
    csv_path = tmp_path / "output.csv"
    expenses = zuzu.record("Expenses", name="", amount=0.0)
    count = expenses.export_csv(str(csv_path))
    assert count == 0


def test_roundtrip_csv(tmp_path):
    """Export then import should preserve data."""
    csv_path = tmp_path / "roundtrip.csv"

    original = zuzu.record("Items", name="", price=0.0)
    original.add(name="Apple", price=1.50)
    original.add(name="Banana", price=0.75)
    original.export_csv(str(csv_path))

    # Reset registry and db for a clean import
    from zuzu.record import _registry
    _registry.clear()
    set_db_path(tmp_path / "test2.db")

    imported = zuzu.record("Items", name="", price=0.0)
    count = imported.import_csv(str(csv_path))
    assert count == 2
    rows = imported.all()
    names = {r["name"] for r in rows}
    assert names == {"Apple", "Banana"}


def test_export_csv_excludes_computed(tmp_path):
    """Computed fields should not appear in CSV export."""
    csv_path = tmp_path / "output.csv"
    items = zuzu.record("Items",
        name="",
        price=0.0,
        label=lambda r: f"Item: {r['name']}",
    )
    items.add(name="Widget", price=9.99)
    items.export_csv(str(csv_path))

    content = csv_path.read_text()
    assert "label" not in content
