"""TDD cycle 4: batch_update() optimization.

The code review identified that batch_update() builds a full coerced_mapping dict
even when self._coerce is False, wasting O(n) memory/time.

These tests are written FIRST (they should FAIL before the fix).
After the fix they must all PASS.
"""

import inspect

import pytest

from nanasqlite import (
    AsyncNanaSQLite,
    NanaSQLite,
)
from nanasqlite import async_core as async_core_module
from nanasqlite import compat as compat_module
from nanasqlite import core as core_module
from nanasqlite.exceptions import NanaSQLiteValidationError


class TestBatchUpdateCodeStructure:
    """Verify batch_update() has the optimized two-path structure."""

    def test_batch_update_does_not_use_ternary_coerce_pattern(self):
        """batch_update should NOT use 'coerced if self._coerce else value' anti-pattern.

        The optimized code separates the coerce vs validate-only loops entirely,
        so no unnecessary dict allocation occurs when coerce=False.
        """
        source = inspect.getsource(NanaSQLite.batch_update)
        assert "coerced if self._coerce else value" not in source, (
            "batch_update() should not build an unnecessary dict when coerce=False. "
            "Use separate code paths: build coerced_mapping only when self._coerce is True."
        )

    def test_batch_update_has_separate_coerce_branch(self):
        """batch_update should have an explicit 'if self._coerce:' branch."""
        source = inspect.getsource(NanaSQLite.batch_update)
        assert "if self._coerce:" in source, (
            "batch_update() should have an explicit 'if self._coerce:' branch "
            "to avoid unnecessary dict allocation when not coercing."
        )

    def test_batch_update_validate_only_branch_does_not_store_coerced(self):
        """The validate-only branch must not store coerced values in a new dict.

        When coerce=False, validation should iterate and call validkit_validate
        without allocating a new mapping dict.
        """
        source = inspect.getsource(NanaSQLite.batch_update)
        # The validate-only (else) branch must not contain 'coerced_mapping[key] = '
        # after the 'else:' for the self._coerce check.
        # A simpler check: the source should NOT have both 'coerced_mapping[key] ='
        # in a context where self._coerce is False.
        # We verify by checking that the logic separates coerce and validate-only.
        # The old pattern is: single loop with 'coerced_mapping[key] = coerced if self._coerce else value'
        # The new pattern must not have this combined ternary.
        assert "coerced if self._coerce else value" not in source, (
            "The validate-only path should not store values in a new dict. "
            "Only the coerce path (if self._coerce:) should build coerced_mapping."
        )


class TestBatchUpdateBehaviourWithMockedValidkit:
    """Verify batch_update() behavior with a mock validator (validkit not installed)."""

    def _patch_validkit(self, monkeypatch, mock_fn):
        """Helper: enable HAS_VALIDKIT and patch validkit_validate on the module."""
        # Patch all potential source modules to ensure lazy imports pick up the mock
        for mod in [core_module, async_core_module, compat_module]:
            monkeypatch.setattr(mod, "HAS_VALIDKIT", True, raising=False)
            monkeypatch.setattr(mod, "validkit_validate", mock_fn, raising=False)

    def test_batch_update_coerce_false_preserves_original_values(self, tmp_path, monkeypatch):
        """With coerce=False and a validator, batch_update should store original values."""
        coerced_sentinel = object()
        calls: list[tuple] = []

        def mock_validate(value, _schema):
            calls.append((value, _schema))
            return coerced_sentinel  # always return a different object

        self._patch_validkit(monkeypatch, mock_validate)

        db_path = str(tmp_path / "test_coerce_false.db")
        db = NanaSQLite(db_path, validator={"type": "string"}, coerce=False)

        db.batch_update({"key1": "hello", "key2": "world"})

        # Validator should have been called for each key
        assert len(calls) == 2

        # Original values (not coerced_sentinel) should be stored
        assert db["key1"] == "hello"
        assert db["key2"] == "world"

    def test_batch_update_coerce_true_stores_coerced_values(self, tmp_path, monkeypatch):
        """With coerce=True and a validator, batch_update should store coerced values."""

        def mock_validate(value, _schema):
            return str(value) + "_coerced"

        self._patch_validkit(monkeypatch, mock_validate)

        db_path = str(tmp_path / "test_coerce_true.db")
        db = NanaSQLite(db_path, validator={"type": "string"}, coerce=True)

        db.batch_update({"key1": "hello", "key2": "world"})

        # Coerced values should be stored
        assert db["key1"] == "hello_coerced"
        assert db["key2"] == "world_coerced"

    def test_batch_update_validates_all_before_writing_when_coerce_false(self, tmp_path, monkeypatch):
        """batch_update with coerce=False must validate all items atomically.

        If any item fails validation, nothing should be written.
        """

        def mock_validate(value, _schema):
            if value == "bad":
                raise ValueError("invalid value")
            return value

        self._patch_validkit(monkeypatch, mock_validate)

        db_path = str(tmp_path / "test_atomic.db")
        db = NanaSQLite(db_path, validator={"type": "string"}, coerce=False)

        with pytest.raises(NanaSQLiteValidationError):
            db.batch_update({"key1": "good", "key2": "bad", "key3": "also_good"})

        # Nothing should be written because validation failed
        assert "key1" not in db
        assert "key2" not in db
        assert "key3" not in db

    def test_batch_update_validates_all_before_writing_when_coerce_true(self, tmp_path, monkeypatch):
        """batch_update with coerce=True must validate all items atomically.

        If any item fails coercion, nothing should be written.
        """

        def mock_validate(value, _schema):
            if value == "bad":
                raise ValueError("invalid value")
            return str(value) + "_coerced"

        self._patch_validkit(monkeypatch, mock_validate)

        db_path = str(tmp_path / "test_atomic_coerce.db")
        db = NanaSQLite(db_path, validator={"type": "string"}, coerce=True)

        with pytest.raises(NanaSQLiteValidationError):
            db.batch_update({"key1": "good", "key2": "bad", "key3": "also_good"})

        # Nothing should be written because coercion failed
        assert "key1" not in db
        assert "key2" not in db
        assert "key3" not in db

    def test_batch_update_partial_writes_only_valid_items_when_coerce_false(self, tmp_path, monkeypatch):
        """batch_update_partial() should skip only invalid items and keep valid ones."""

        def mock_validate(value, _schema):
            if value == "bad":
                raise ValueError("invalid value")
            return f"{value}_coerced"

        self._patch_validkit(monkeypatch, mock_validate)

        db = NanaSQLite(str(tmp_path / "test_partial.db"), validator={"type": "string"}, coerce=False)
        failed = db.batch_update_partial({"key1": "good", "key2": "bad", "key3": "also_good"})

        assert set(failed) == {"key2"}
        assert "failed schema validation" in failed["key2"]
        assert db["key1"] == "good"
        assert db["key3"] == "also_good"
        assert "key2" not in db

    def test_batch_update_partial_writes_coerced_valid_items_when_coerce_true(self, tmp_path, monkeypatch):
        """batch_update_partial() should keep coerced valid items and skip only invalid ones."""

        def mock_validate(value, _schema):
            if value == "bad":
                raise ValueError("invalid value")
            return str(value) + "_coerced"

        self._patch_validkit(monkeypatch, mock_validate)

        db = NanaSQLite(str(tmp_path / "test_partial_coerce.db"), validator={"type": "string"}, coerce=True)
        failed = db.batch_update_partial({"key1": "good", "key2": "bad", "key3": "also_good"})

        assert set(failed) == {"key2"}
        assert db["key1"] == "good_coerced"
        assert db["key3"] == "also_good_coerced"
        assert "key2" not in db


@pytest.mark.asyncio
async def test_async_batch_update_partial_writes_only_valid_items(tmp_path, monkeypatch):
    """AsyncNanaSQLite: batch_update_partial() should mirror the sync best-effort behavior."""

    def mock_validate(value, _schema):
        if value == "bad":
            raise ValueError("invalid value")
        return value

    monkeypatch.setattr(core_module, "HAS_VALIDKIT", True)
    monkeypatch.setattr(async_core_module, "HAS_VALIDKIT", True)
    monkeypatch.setattr(compat_module, "HAS_VALIDKIT", True)
    monkeypatch.setattr(core_module, "validkit_validate", mock_validate, raising=False)
    monkeypatch.setattr(compat_module, "validkit_validate", mock_validate, raising=False)
    monkeypatch.setattr(async_core_module, "validkit_validate", mock_validate, raising=False)

    async with AsyncNanaSQLite(str(tmp_path / "test_async_partial.db"), validator={"type": "string"}) as db:
        failed = await db.abatch_update_partial({"key1": "good", "key2": "bad", "key3": "also_good"})

        assert set(failed) == {"key2"}
        assert await db.aget("key1") == "good"
        assert await db.aget("key3") == "also_good"
        assert not await db.acontains("key2")
