"""Tests for automatic timestamp injection in DBClient convenience methods."""
from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock

import pytest

from db_core.clients.base import DBClient


# ---------------------------------------------------------------------------
# Minimal concrete DBClient for testing timestamp injection
# ---------------------------------------------------------------------------

class FakeDBClient(DBClient):
    """Concrete DBClient that stubs all abstract methods for testing."""

    def __init__(self, auto_timestamps: bool = True):
        self._auto_timestamps = auto_timestamps

    async def connect(self): ...

    async def disconnect(self): ...

    def table(self, name: str):
        from db_core import QueryBuilder
        return QueryBuilder(self, name)

    async def _execute(self, query):
        return MagicMock()

    async def execute_raw(self, sql, params=None, *, multi_statement=False):
        return []

    # Stub the internal mutation methods so convenience methods work
    async def _insert_one(self, table, data, returning=None):
        return dict(data)

    async def _insert_many(self, table, data, returning=None):
        return [dict(d) for d in data]

    async def _update(self, table, conditions, values, limit=None):
        return 1

    async def _bulk_update(self, table, ids, values):
        return len(ids)

    async def _delete(self, table, conditions, limit=None):
        return 1

    async def find_by_id(self, table, id, *, id_field="id"):
        return None  # Always "not found" so save() creates


# ---------------------------------------------------------------------------
# Helper: freeze time so assertions are deterministic
# ---------------------------------------------------------------------------

@pytest.fixture
def frozen_now():
    """Return a fixed UTC datetime for deterministic assertions."""
    return datetime(2026, 6, 11, 12, 0, 0, tzinfo=timezone.utc)


@pytest.fixture
def client(frozen_now):
    """Client with a frozen clock."""
    c = FakeDBClient()
    c._now = MagicMock(return_value=frozen_now)
    return c


# ===========================================================================
# create() — injects both created_at and updated_at
# ===========================================================================


async def test_create_injects_both_timestamps(client, frozen_now):
    """create() adds created_at and updated_at when missing."""
    result = await client.create("users", {"name": "Alice"})

    assert result["name"] == "Alice"
    assert result["created_at"] == frozen_now
    assert result["updated_at"] == frozen_now


async def test_create_respects_explicit_created_at(client, frozen_now):
    """create() does NOT overwrite user-provided created_at."""
    custom_ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
    result = await client.create("users", {"name": "Bob", "created_at": custom_ts})

    assert result["created_at"] == custom_ts
    assert result["updated_at"] == frozen_now


async def test_create_respects_explicit_updated_at(client, frozen_now):
    """create() does NOT overwrite user-provided updated_at."""
    custom_ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
    result = await client.create("users", {"name": "Bob", "updated_at": custom_ts})

    assert result["created_at"] == frozen_now
    assert result["updated_at"] == custom_ts


# ===========================================================================
# create_many() — injects per-item
# ===========================================================================


async def test_create_many_injects_per_item(client, frozen_now):
    """create_many() injects timestamps into each dict in the list."""
    results = await client.create_many("users", [
        {"name": "Alice"},
        {"name": "Bob"},
    ])

    assert len(results) == 2
    assert results[0]["created_at"] == frozen_now
    assert results[0]["updated_at"] == frozen_now
    assert results[1]["created_at"] == frozen_now
    assert results[1]["updated_at"] == frozen_now


async def test_create_many_respects_explicit(client, frozen_now):
    """create_many() does not overwrite user timestamps."""
    custom = datetime(2025, 6, 1, tzinfo=timezone.utc)
    results = await client.create_many("users", [
        {"name": "Alice", "created_at": custom},
        {"name": "Bob"},
    ])

    assert results[0]["created_at"] == custom
    assert results[0]["updated_at"] == frozen_now
    assert results[1]["created_at"] == frozen_now


# ===========================================================================
# update() — injects updated_at only
# ===========================================================================


async def test_update_injects_updated_at(client, frozen_now):
    """update() adds updated_at but NOT created_at."""
    # Spy on _update to capture the values passed to it
    client._update = AsyncMock(return_value=1)

    await client.update("users", {"id": 1}, {"name": "Updated"})

    # Verify _update was called with injected updated_at
    call_args = client._update.call_args
    values = call_args[0][2]  # 3rd positional arg = values
    assert "updated_at" in values
    assert values["updated_at"] == frozen_now
    assert "created_at" not in values


async def test_update_respects_explicit_updated_at(client, frozen_now):
    """update() does not overwrite explicit updated_at."""
    custom = datetime(2025, 3, 15, tzinfo=timezone.utc)
    client._update = AsyncMock(return_value=1)

    await client.update("users", {"id": 1}, {"name": "X", "updated_at": custom})

    call_args = client._update.call_args
    values = call_args[0][2]
    assert values["updated_at"] == custom


# ===========================================================================
# update_by_id() — injects updated_at only
# ===========================================================================


async def test_update_by_id_injects_updated_at(client, frozen_now):
    """update_by_id() adds updated_at."""
    client._update = AsyncMock(return_value=1)

    await client.update_by_id("users", 1, {"name": "Updated"})

    call_args = client._update.call_args
    values = call_args[0][2]
    assert "updated_at" in values
    assert values["updated_at"] == frozen_now


# ===========================================================================
# save() — injects updated_at only (not created_at)
# ===========================================================================


async def test_save_injects_updated_at_only(client, frozen_now):
    """save() adds updated_at but NOT created_at."""
    result = await client.save("users", {"name": "Upserted"})

    assert "updated_at" in result
    assert result["updated_at"] == frozen_now


async def test_save_respects_explicit_updated_at(client, frozen_now):
    """save() does not overwrite explicit updated_at."""
    custom = datetime(2025, 9, 1, tzinfo=timezone.utc)
    result = await client.save("users", {"name": "X", "updated_at": custom})

    assert result["updated_at"] == custom


# ===========================================================================
# bulk_update() — injects updated_at only
# ===========================================================================


async def test_bulk_update_injects_updated_at(client, frozen_now):
    """bulk_update() adds updated_at."""
    client._bulk_update = AsyncMock(return_value=3)

    await client.bulk_update("users", [1, 2, 3], {"status": "active"})

    call_args = client._bulk_update.call_args
    values = call_args[0][2]
    assert "updated_at" in values
    assert values["updated_at"] == frozen_now


# ===========================================================================
# Opt-out: _auto_timestamps = False
# ===========================================================================


async def test_auto_timestamps_false_skips_create():
    """When _auto_timestamps=False, create() does NOT inject timestamps."""
    c = FakeDBClient(auto_timestamps=False)
    result = await c.create("users", {"name": "NoTimestamps"})

    assert "created_at" not in result
    assert "updated_at" not in result


async def test_auto_timestamps_false_skips_update(frozen_now):
    """When _auto_timestamps=False, update() does NOT inject timestamps."""
    c = FakeDBClient(auto_timestamps=False)
    c._update = AsyncMock(return_value=1)

    await c.update("users", {"id": 1}, {"name": "X"})

    call_args = c._update.call_args
    values = call_args[0][2]
    assert "updated_at" not in values


async def test_auto_timestamps_false_skips_save():
    """When _auto_timestamps=False, save() does NOT inject timestamps."""
    c = FakeDBClient(auto_timestamps=False)
    result = await c.save("users", {"name": "NoTimestamps"})

    assert "updated_at" not in result
