"""Tests for BaseRepository CRUD methods (find_by_id, first, find, all, count,
exists, insert_one, insert_many, save, update, update_by_id, remove, remove_by_id,
and their db_-prefixed aliases)."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from repo_core import BaseRepository


# ---------------------------------------------------------------------------
# Minimal repository for testing
# ---------------------------------------------------------------------------

class CrudRepo(BaseRepository[dict]):
    _backends = set()  # no auto-wiring


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def repo(db_query):
    """Repository with a mocked DB QueryBuilder."""
    return CrudRepo(db=db_query)


@pytest.fixture
def repo_with_model(db_query):
    """Repository with a DomainModel for _from_db conversion testing."""
    from repo_core import DomainModel

    class FakeModel(DomainModel):
        id: int
        name: str

    return CrudRepo(model=FakeModel, db=db_query)


# ===========================================================================
# find_by_id
# ===========================================================================

async def test_find_by_id_returns_row(repo, db_query):
    """find_by_id uses table().where(id_field, id).first()."""
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"id": 1, "name": "Alice"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find_by_id("users", 1)
    assert result == {"id": 1, "name": "Alice"}


async def test_find_by_id_returns_none_when_not_found(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value=None)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find_by_id("users", 999)
    assert result is None


async def test_find_by_id_custom_id_field(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"uuid": "abc"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find_by_id("users", "abc", id_field="uuid")
    assert result == {"uuid": "abc"}


async def test_find_by_id_with_model_conversion(repo_with_model, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"id": 7, "name": "Bob"})
    repo_with_model.table = MagicMock(return_value=qb_mock)

    result = await repo_with_model.find_by_id("users", 7)
    assert result.id == 7
    assert result.name == "Bob"


# ===========================================================================
# first
# ===========================================================================

async def test_first_returns_matching_row(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"id": 2, "name": "Eve"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.first("users", {"name": "Eve"})
    assert result == {"id": 2, "name": "Eve"}
    qb_mock.first.assert_awaited_once()


async def test_first_returns_none_when_no_match(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value=None)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.first("users", {"name": "Nobody"})
    assert result is None


async def test_first_multiple_conditions(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"id": 3})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.first("users", {"status": "active", "role": "admin"})
    assert qb_mock.where.call_count == 2


# ===========================================================================
# find
# ===========================================================================

async def test_find_returns_all_matching(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.order_by = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.offset = MagicMock(return_value=qb_mock)
    qb_mock.all = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find("users", {"status": "active"})
    assert len(result) == 2


async def test_find_no_conditions(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.all = AsyncMock(return_value=[{"id": 1}, {"id": 2}, {"id": 3}])
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find("users")
    assert len(result) == 3
    qb_mock.where.assert_not_called()


async def test_find_with_pagination(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.order_by = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.offset = MagicMock(return_value=qb_mock)
    qb_mock.all = AsyncMock(return_value=[{"id": 10}])
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.find("users", limit=10, offset=20, order_by=["name"])
    assert len(result) == 1
    qb_mock.limit.assert_called_once_with(10)
    qb_mock.offset.assert_called_once_with(20)
    qb_mock.order_by.assert_called_once_with("name")


async def test_find_with_model_conversion(repo_with_model, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.order_by = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.offset = MagicMock(return_value=qb_mock)
    qb_mock.all = AsyncMock(return_value=[{"id": 1, "name": "A"}, {"id": 2, "name": "B"}])
    repo_with_model.table = MagicMock(return_value=qb_mock)

    result = await repo_with_model.find("users")
    assert all(hasattr(r, "id") for r in result)
    assert [r.name for r in result] == ["A", "B"]


# ===========================================================================
# all
# ===========================================================================

async def test_all_returns_all_rows(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.all = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.all("users")
    assert len(result) == 2


async def test_all_with_pagination(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.order_by = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.offset = MagicMock(return_value=qb_mock)
    qb_mock.all = AsyncMock(return_value=[])
    repo.table = MagicMock(return_value=qb_mock)

    await repo.all("users", limit=5, offset=0, order_by=["-created_at"])
    qb_mock.limit.assert_called_once_with(5)
    qb_mock.offset.assert_called_once_with(0)
    qb_mock.order_by.assert_called_once_with("-created_at")


# ===========================================================================
# count
# ===========================================================================

async def test_count_returns_row_count(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.count = AsyncMock(return_value=42)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.count("users")
    assert result == 42


async def test_count_with_conditions(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.count = AsyncMock(return_value=5)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.count("users", {"status": "active"})
    assert result == 5
    qb_mock.where.assert_called_once()


# ===========================================================================
# exists
# ===========================================================================

async def test_exists_returns_true_when_rows_present(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.count = AsyncMock(return_value=3)
    repo.table = MagicMock(return_value=qb_mock)

    assert await repo.exists("users", {"status": "active"}) is True


async def test_exists_returns_false_when_no_rows(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.count = AsyncMock(return_value=0)
    repo.table = MagicMock(return_value=qb_mock)

    assert await repo.exists("users", {"status": "banned"}) is False


# ===========================================================================
# insert_one
# ===========================================================================

async def test_insert_one_creates_and_returns_row(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1, "name": "New"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.insert_one("users", {"name": "New"})
    assert result == {"id": 1, "name": "New"}
    qb_mock.create.assert_awaited_once_with({"name": "New"}, returning=["*"])


async def test_insert_one_with_returning(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 2, "name": "Alice", "email": "a@b.com"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.insert_one("users", {"name": "Alice"}, returning=["id", "name", "email"])
    assert result == {"id": 2, "name": "Alice", "email": "a@b.com"}
    qb_mock.create.assert_awaited_once_with({"name": "Alice"}, returning=["id", "name", "email"])


async def test_insert_one_with_model_conversion(repo_with_model, db_query):
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 99, "name": "Zoe"})
    repo_with_model.table = MagicMock(return_value=qb_mock)

    result = await repo_with_model.insert_one("users", {"name": "Zoe"})
    assert result.id == 99
    assert result.name == "Zoe"


# ===========================================================================
# insert_many
# ===========================================================================

async def test_insert_many_creates_and_returns_rows(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.create_many = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.insert_many("users", [{"name": "A"}, {"name": "B"}])
    assert len(result) == 2
    qb_mock.create_many.assert_awaited_once_with([{"name": "A"}, {"name": "B"}], returning=["*"])


async def test_insert_many_with_model_conversion(repo_with_model, db_query):
    qb_mock = MagicMock()
    qb_mock.create_many = AsyncMock(return_value=[{"id": 1, "name": "X"}, {"id": 2, "name": "Y"}])
    repo_with_model.table = MagicMock(return_value=qb_mock)

    result = await repo_with_model.insert_many("users", [{"name": "X"}, {"name": "Y"}])
    assert len(result) == 2
    assert result[0].name == "X"
    assert result[1].name == "Y"


# ===========================================================================
# save (upsert)
# ===========================================================================

async def test_save_inserts_new_row(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.save = AsyncMock(return_value={"id": 10, "name": "Saved"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.save("users", {"name": "Saved"})
    assert result == {"id": 10, "name": "Saved"}
    qb_mock.save.assert_awaited_once_with({"name": "Saved"}, id_field="id", returning=["*"])


async def test_save_with_custom_id_field(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.save = AsyncMock(return_value={"uuid": "xyz", "name": "Saved"})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.save("users", {"uuid": "xyz", "name": "Saved"}, id_field="uuid")
    qb_mock.save.assert_awaited_once_with(
        {"uuid": "xyz", "name": "Saved"}, id_field="uuid", returning=["*"]
    )


async def test_save_with_model_conversion(repo_with_model, db_query):
    qb_mock = MagicMock()
    qb_mock.save = AsyncMock(return_value={"id": 5, "name": "ModelSave"})
    repo_with_model.table = MagicMock(return_value=qb_mock)

    result = await repo_with_model.save("users", {"name": "ModelSave"})
    assert result.id == 5
    assert result.name == "ModelSave"


# ===========================================================================
# update
# ===========================================================================

async def test_update_returns_row_count(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=3)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.update("users", {"status": "inactive"}, {"status": "active"})
    assert result == 3
    qb_mock.update.assert_awaited_once_with(status="active")


async def test_update_with_limit(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.update("users", {"status": "inactive"}, {"status": "active"}, limit=1)
    qb_mock.limit.assert_called_once_with(1)


async def test_update_multiple_conditions(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=2)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.update("users", {"status": "inactive", "role": "user"}, {"status": "active"})
    assert qb_mock.where.call_count == 2


# ===========================================================================
# update_by_id
# ===========================================================================

async def test_update_by_id_returns_refreshed_row(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=1)
    qb_mock.first = AsyncMock(return_value={"id": 1, "name": "Updated"})
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.update_by_id("users", 1, {"name": "Updated"})
    assert result == {"id": 1, "name": "Updated"}
    qb_mock.update.assert_awaited_once_with(name="Updated")


async def test_update_by_id_returns_none_when_not_found(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=0)
    qb_mock.first = AsyncMock(return_value=None)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.update_by_id("users", 999, {"name": "Gone"})
    assert result is None


async def test_update_by_id_custom_id_field(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=1)
    qb_mock.first = AsyncMock(return_value={"uuid": "abc"})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.update_by_id("users", "abc", {"name": "X"}, id_field="uuid")
    # Check first where call used "uuid"
    qb_mock.where.assert_any_call("uuid", "abc")


# ===========================================================================
# remove
# ===========================================================================

async def test_remove_returns_row_count(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.delete = AsyncMock(return_value=5)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.remove("users", {"status": "deleted"})
    assert result == 5
    qb_mock.delete.assert_awaited_once()


async def test_remove_with_limit(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.delete = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.remove("users", {"status": "spam"}, limit=100)
    qb_mock.limit.assert_called_once_with(100)


# ===========================================================================
# remove_by_id
# ===========================================================================

async def test_remove_by_id_returns_row_count(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.delete = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.remove_by_id("users", 42)
    assert result == 1
    qb_mock.delete.assert_awaited_once()


async def test_remove_by_id_returns_zero_when_not_found(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.delete = AsyncMock(return_value=0)
    repo.table = MagicMock(return_value=qb_mock)

    result = await repo.remove_by_id("users", 999)
    assert result == 0


async def test_remove_by_id_custom_id_field(repo, db_query):
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.delete = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.remove_by_id("users", "key-123", id_field="api_key")
    qb_mock.where.assert_called_once_with("api_key", "key-123")


# ===========================================================================
# db_-prefixed aliases
# ===========================================================================

async def test_db_aliases_are_callable(repo, db_query):
    """Verify that all db_-prefixed aliases exist and delegate correctly."""
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.order_by = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.offset = MagicMock(return_value=qb_mock)
    qb_mock.all = AsyncMock(return_value=[])
    qb_mock.first = AsyncMock(return_value=None)
    qb_mock.count = AsyncMock(return_value=0)
    qb_mock.create = AsyncMock(return_value={"id": 1})
    qb_mock.create_many = AsyncMock(return_value=[])
    qb_mock.save = AsyncMock(return_value={"id": 1})
    qb_mock.update = AsyncMock(return_value=1)
    qb_mock.delete = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    # db_get → find_by_id → table().where().first()
    await repo.db_get("t", 1)
    qb_mock.first.assert_awaited()

    # db_first
    qb_mock.first.reset_mock()
    await repo.db_first("t", {"x": 1})
    qb_mock.first.assert_awaited()

    # db_find
    await repo.db_find("t")
    qb_mock.all.assert_awaited()

    # db_all
    qb_mock.all.reset_mock()
    await repo.db_all("t")
    qb_mock.all.assert_awaited()

    # db_count
    await repo.db_count("t")
    qb_mock.count.assert_awaited()

    # db_exists
    await repo.db_exists("t", {"x": 1})
    assert qb_mock.count.call_count >= 1

    # db_insert_one
    await repo.db_insert_one("t", {"x": 1})
    qb_mock.create.assert_awaited()

    # db_insert_many
    await repo.db_insert_many("t", [{"x": 1}])
    qb_mock.create_many.assert_awaited()

    # db_save
    await repo.db_save("t", {"x": 1})
    qb_mock.save.assert_awaited()

    # db_update
    await repo.db_update("t", {}, {"x": 2})
    qb_mock.update.assert_awaited()

    # db_remove
    await repo.db_remove("t", {})
    qb_mock.delete.assert_awaited()

    # db_remove_by_id
    qb_mock.delete.reset_mock()
    await repo.db_remove_by_id("t", 1)
    qb_mock.delete.assert_awaited()


# ===========================================================================
# Timestamp auto-injection (model-aware)
# ===========================================================================


async def test_insert_one_model_injects_timestamps(db_query):
    """insert_one injects created_at/updated_at when model has those fields."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        name: str
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1, "name": "Alice"})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.insert_one("t", {"name": "Alice"})

    qb_mock.create.assert_awaited_once()
    data_passed = qb_mock.create.call_args[0][0]
    assert "created_at" in data_passed
    assert "updated_at" in data_passed
    assert data_passed["name"] == "Alice"


async def test_insert_one_respects_explicit_timestamps(db_query):
    """User-provided timestamps are never overwritten."""

    from datetime import datetime, timezone  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1})
    repo.table = MagicMock(return_value=qb_mock)

    custom = datetime(2025, 1, 1, tzinfo=timezone.utc)
    await repo.insert_one("t", {"created_at": custom, "updated_at": custom})

    data = qb_mock.create.call_args[0][0]
    assert data["created_at"] == custom
    assert data["updated_at"] == custom


async def test_insert_many_model_injects_timestamps(db_query):
    """insert_many injects timestamps into each item."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create_many = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
    repo.table = MagicMock(return_value=qb_mock)

    await repo.insert_many("t", [{"name": "A"}, {"name": "B"}])

    qb_mock.create_many.assert_awaited_once()
    items = qb_mock.create_many.call_args[0][0]
    assert len(items) == 2
    for item in items:
        assert "created_at" in item
        assert "updated_at" in item


async def test_update_model_injects_updated_at(db_query):
    """update injects updated_at but NOT created_at."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.limit = MagicMock(return_value=qb_mock)
    qb_mock.update = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.update("t", {"id": 1}, {"name": "Updated"})

    qb_mock.update.assert_awaited_once()
    kwargs = qb_mock.update.call_args[1]  # update(**values) → kwargs
    assert "updated_at" in kwargs
    assert "created_at" not in kwargs
    assert kwargs["name"] == "Updated"


async def test_update_by_id_model_injects_updated_at(db_query):
    """update_by_id injects updated_at."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.where = MagicMock(return_value=qb_mock)
    qb_mock.first = AsyncMock(return_value={"id": 1})
    qb_mock.update = AsyncMock(return_value=1)
    repo.table = MagicMock(return_value=qb_mock)

    await repo.update_by_id("t", 1, {"name": "V2"})

    qb_mock.update.assert_awaited_once()
    kwargs = qb_mock.update.call_args[1]
    assert "updated_at" in kwargs


async def test_save_model_injects_updated_at(db_query):
    """save injects updated_at but NOT created_at."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class TSModel(DomainModel):
        id: int
        created_at: datetime | None = None
        updated_at: datetime | None = None

    TSModel.model_rebuild()
    repo = CrudRepo(model=TSModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.save = AsyncMock(return_value={"id": 1})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.save("t", {"name": "Upsert"})

    qb_mock.save.assert_awaited_once()
    data = qb_mock.save.call_args[0][0]
    assert "updated_at" in data


async def test_insert_one_no_model_skips_injection(db_query):
    """When model=None, timestamps are NOT injected."""

    repo = CrudRepo(model=None, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.insert_one("t", {"name": "Raw"})

    data = qb_mock.create.call_args[0][0]
    assert "created_at" not in data
    assert "updated_at" not in data


async def test_model_without_timestamps_unaffected(db_query):
    """Model without created_at/updated_at fields gets no injection."""

    from repo_core import DomainModel

    class PlainModel(DomainModel):
        id: int
        name: str

    PlainModel.model_rebuild()
    repo = CrudRepo(model=PlainModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1, "name": "Plain"})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.insert_one("t", {"name": "Plain"})

    data = qb_mock.create.call_args[0][0]
    assert "created_at" not in data
    assert "updated_at" not in data


async def test_model_with_only_created_at(db_query):
    """Model with only created_at (no updated_at) gets only created_at injected."""

    from datetime import datetime  # noqa: F811
    from repo_core import DomainModel

    class CreatedOnlyModel(DomainModel):
        id: int
        created_at: datetime | None = None

    CreatedOnlyModel.model_rebuild()
    repo = CrudRepo(model=CreatedOnlyModel, db=db_query)
    qb_mock = MagicMock()
    qb_mock.create = AsyncMock(return_value={"id": 1})
    repo.table = MagicMock(return_value=qb_mock)

    await repo.insert_one("t", {"name": "CreatedOnly"})

    data = qb_mock.create.call_args[0][0]
    assert "created_at" in data
    assert "updated_at" not in data
