"""Integration tests for SqlAlchemyPreferencesRepository.

Tests key-value preference storage including:
- Basic get/set/delete operations
- Type coercion (get_bool)
- Value updates (upsert behavior)
- JSONB value storage
- Edge cases and data integrity
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

import pytest
from _pytest.python_api import ApproxBase

from noteflow.infrastructure.persistence.repositories import SqlAlchemyPreferencesRepository


class _ApproxCallable:
    """Protocol for pytest.approx with explicit parameter types."""

    def __call__(
        self,
        expected: float,
        *,
        rel: float | None = None,
        abs: float | None = None,
        nan_ok: bool = False,
    ) -> ApproxBase:
        """Call signature matching pytest.approx for float values."""
        ...


def _approx_float(
    expected: float, *, rel: float | None = None, abs: float | None = None
) -> ApproxBase:
    """Type-safe wrapper for pytest.approx with float values.

    Provides explicit type annotations to avoid basedpyright reportUnknownMemberType
    errors caused by pytest.approx having partially unknown parameter types.

    Cast required: pytest.approx has untyped parameters (expected, rel, abs) in its
    signature. The return type ApproxBase is correctly typed, but pyright reports
    the function type as partially unknown due to the untyped parameters.
    """
    _approx: _ApproxCallable = cast(_ApproxCallable, pytest.approx)
    return _approx(expected, rel=rel, abs=abs)

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession

# ============================================================================
# Test Constants
# ============================================================================

# Volume settings
VOLUME_INITIAL = 50
VOLUME_UPDATED = 75

# Audio input volume setting
AUDIO_INPUT_VOLUME = 80

# Large data test size
LARGE_LIST_SIZE = 1000

# Float precision test value
AUDIO_GAIN_PRECISION_VALUE = 0.123456789


@pytest.mark.integration
class TestPreferencesRepositoryBasicOperations:
    """Integration tests for basic preferences CRUD operations."""

    async def test_set_and_get_string_value(self, session: AsyncSession) -> None:
        """Test setting and getting a string preference value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("theme", "dark")
        await session.commit()

        value = await repo.get("theme")

        assert value == "dark", f"expected theme to be 'dark', got {value!r}"

    async def test_set_and_get_integer_value(self, session: AsyncSession) -> None:
        """Test setting and getting an integer preference value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("max_recordings", 100)
        await session.commit()

        value = await repo.get("max_recordings")

        assert value == 100, f"expected max_recordings to be 100, got {value}"

    async def test_set_and_get_boolean_value(self, session: AsyncSession) -> None:
        """Test setting and getting a boolean preference value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("notifications_enabled", True)
        await session.commit()

        value = await repo.get("notifications_enabled")

        assert value is True, f"expected notifications_enabled to be True, got {value}"

    async def test_set_and_get_list_value(self, session: AsyncSession) -> None:
        """Test setting and getting a list preference value."""
        repo = SqlAlchemyPreferencesRepository(session)

        languages = ["en", "es", "fr"]
        await repo.set("preferred_languages", languages)
        await session.commit()

        value = await repo.get("preferred_languages")

        assert value == languages, f"expected preferred_languages to be {languages}, got {value}"

    async def test_set_and_get_dict_value(self, session: AsyncSession) -> None:
        """Test setting and getting a dict preference value."""
        repo = SqlAlchemyPreferencesRepository(session)

        shortcuts = {"record": "Ctrl+R", "stop": "Ctrl+S"}
        await repo.set("keyboard_shortcuts", shortcuts)
        await session.commit()

        value = await repo.get("keyboard_shortcuts")

        assert value == shortcuts, f"expected keyboard_shortcuts to be {shortcuts}, got {value}"

    async def test_get_nonexistent_key_returns_none(self, session: AsyncSession) -> None:
        """Test getting a key that doesn't exist returns None."""
        repo = SqlAlchemyPreferencesRepository(session)

        value = await repo.get("nonexistent_key")

        assert value is None, f"expected nonexistent key to return None, got {value!r}"

    async def test_set_updates_existing_value(self, session: AsyncSession) -> None:
        """Test setting a key that already exists updates the value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("volume", VOLUME_INITIAL)
        await session.commit()

        await repo.set("volume", VOLUME_UPDATED)
        await session.commit()

        value = await repo.get("volume")

        assert value == VOLUME_UPDATED, f"expected volume to be updated to {VOLUME_UPDATED}, got {value}"

    async def test_set_null_value(self, session: AsyncSession) -> None:
        """Test setting a None value explicitly."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("optional_setting", None)
        await session.commit()

        value = await repo.get("optional_setting")

        assert value is None, f"expected explicitly set None value to return None, got {value!r}"


@pytest.mark.integration
class TestPreferencesRepositoryBooleanCoercion:
    """Integration tests for get_bool type coercion."""

    async def test_get_bool_with_true_value(self, session: AsyncSession) -> None:
        """Test get_bool returns True for truthy boolean value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("cloud_consent_granted", True)
        await session.commit()

        value = await repo.get_bool("cloud_consent_granted")

        assert value is True, f"expected get_bool to return True for truthy value, got {value}"

    async def test_get_bool_with_false_value(self, session: AsyncSession) -> None:
        """Test get_bool returns False for falsy boolean value."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("cloud_consent_granted", False)
        await session.commit()

        value = await repo.get_bool("cloud_consent_granted")

        assert value is False, f"expected get_bool to return False for falsy value, got {value}"

    async def test_get_bool_with_default_when_missing(self, session: AsyncSession) -> None:
        """Test get_bool returns default when key doesn't exist."""
        repo = SqlAlchemyPreferencesRepository(session)

        value_default_false = await repo.get_bool("nonexistent")
        value_default_true = await repo.get_bool("nonexistent", default=True)

        assert value_default_false is False, f"expected default False when key missing, got {value_default_false}"
        assert value_default_true is True, f"expected default True when explicitly passed, got {value_default_true}"

    async def test_get_bool_coerces_truthy_integer(self, session: AsyncSession) -> None:
        """Test get_bool coerces non-zero integer to True."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("numeric_flag", 1)
        await session.commit()

        value = await repo.get_bool("numeric_flag")

        assert value is True, f"expected non-zero integer to coerce to True, got {value}"

    async def test_get_bool_coerces_falsy_integer(self, session: AsyncSession) -> None:
        """Test get_bool coerces zero to False."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("numeric_flag", 0)
        await session.commit()

        value = await repo.get_bool("numeric_flag")

        assert value is False, f"expected zero to coerce to False, got {value}"

    async def test_get_bool_coerces_truthy_string(self, session: AsyncSession) -> None:
        """Test get_bool coerces non-empty string to True."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("string_flag", "yes")
        await session.commit()

        value = await repo.get_bool("string_flag")

        assert value is True, f"expected non-empty string to coerce to True, got {value}"

    async def test_get_bool_coerces_empty_string(self, session: AsyncSession) -> None:
        """Test get_bool coerces empty string to False."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("string_flag", "")
        await session.commit()

        value = await repo.get_bool("string_flag")

        assert value is False, f"expected empty string to coerce to False, got {value}"


@pytest.mark.integration
class TestPreferencesRepositoryDelete:
    """Integration tests for preference deletion."""

    async def test_delete_existing_key(self, session: AsyncSession) -> None:
        """Test deleting an existing preference returns True."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("to_delete", "value")
        await session.commit()

        result = await repo.delete("to_delete")
        await session.commit()

        assert result is True, f"expected delete to return True for existing key, got {result}"

        value = await repo.get("to_delete")
        assert value is None, f"expected deleted key to return None, got {value!r}"

    async def test_delete_nonexistent_key(self, session: AsyncSession) -> None:
        """Test deleting a key that doesn't exist returns False."""
        repo = SqlAlchemyPreferencesRepository(session)

        result = await repo.delete("nonexistent")
        await session.commit()

        assert result is False, f"expected delete to return False for nonexistent key, got {result}"

    async def test_delete_then_recreate(self, session: AsyncSession) -> None:
        """Test that a deleted key can be recreated."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("recyclable", "original")
        await session.commit()

        await repo.delete("recyclable")
        await session.commit()

        await repo.set("recyclable", "new")
        await session.commit()

        value = await repo.get("recyclable")

        assert value == "new", f"expected recreated key to have value 'new', got {value!r}"


@pytest.mark.integration
class TestPreferencesRepositoryIsolation:
    """Integration tests for preference isolation between keys."""

    async def test_multiple_keys_independent(self, session: AsyncSession) -> None:
        """Test multiple preference keys are independent."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("key1", "value1")
        await repo.set("key2", "value2")
        await repo.set("key3", "value3")
        await session.commit()

        assert await repo.get("key1") == "value1", "key1 should have independent value 'value1'"
        assert await repo.get("key2") == "value2", "key2 should have independent value 'value2'"
        assert await repo.get("key3") == "value3", "key3 should have independent value 'value3'"

    async def test_updating_one_key_doesnt_affect_others(self, session: AsyncSession) -> None:
        """Test updating one key doesn't affect other keys."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("stable", "unchanged")
        await repo.set("changing", "original")
        await session.commit()

        await repo.set("changing", "updated")
        await session.commit()

        assert await repo.get("stable") == "unchanged", "stable key should remain unchanged"
        assert await repo.get("changing") == "updated", "changing key should be updated"

    async def test_deleting_one_key_doesnt_affect_others(self, session: AsyncSession) -> None:
        """Test deleting one key doesn't affect other keys."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("keeper", "kept")
        await repo.set("goner", "gone")
        await session.commit()

        await repo.delete("goner")
        await session.commit()

        assert await repo.get("keeper") == "kept", "keeper key should remain after deleting another key"
        assert await repo.get("goner") is None, "goner key should be deleted"


@pytest.mark.integration
class TestPreferencesRepositoryEdgeCases:
    """Integration tests for edge cases and data integrity."""

    async def test_empty_string_key(self, session: AsyncSession) -> None:
        """Test that empty string key works correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("", "empty_key_value")
        await session.commit()

        value = await repo.get("")

        assert value == "empty_key_value", f"expected empty string key to store value correctly, got {value!r}"

    async def test_special_characters_in_key(self, session: AsyncSession) -> None:
        """Test keys with special characters work correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        special_key = "user.settings.audio-quality_v2"
        await repo.set(special_key, "high")
        await session.commit()

        value = await repo.get(special_key)

        assert value == "high", f"expected special character key to work, got {value!r}"

    async def test_unicode_value(self, session: AsyncSession) -> None:
        """Test storing unicode values works correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("greeting", "Hello, World!")
        await session.commit()

        value = await repo.get("greeting")

        assert value == "Hello, World!", f"expected unicode value to be preserved, got {value!r}"

    async def test_nested_dict_value(self, session: AsyncSession) -> None:
        """Test storing nested dictionaries works correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        nested = {
            "audio": {
                "input": {"device": "default", "volume": AUDIO_INPUT_VOLUME},
                "output": {"device": "speakers", "volume": 60},
            },
            "video": {"resolution": "1080p"},
        }
        await repo.set("media_settings", nested)
        await session.commit()

        value = await repo.get("media_settings")

        assert value == nested, "should return complete nested dict"
        assert isinstance(value, dict), "value should be dict type"
        audio_settings = cast(dict[str, object], value["audio"])
        assert isinstance(audio_settings, dict), "nested audio should be dict"
        input_settings = cast(dict[str, object], audio_settings["input"])
        assert isinstance(input_settings, dict), "deeply nested input should be dict"
        assert input_settings["volume"] == AUDIO_INPUT_VOLUME, "nested value should be preserved"

    async def test_large_value(self, session: AsyncSession) -> None:
        """Test storing large values works correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        large_list = list(range(LARGE_LIST_SIZE))
        await repo.set("large_data", large_list)
        await session.commit()

        value = await repo.get("large_data")

        assert value == large_list, "expected large list to be preserved exactly"
        assert isinstance(value, list), f"expected list type, got {type(value)}"
        value_list = cast(list[object], value)
        assert len(value_list) == LARGE_LIST_SIZE, f"expected list length of {LARGE_LIST_SIZE}, got {len(value_list)}"

    async def test_float_value_precision(self, session: AsyncSession) -> None:
        """Test float values maintain precision through JSONB storage."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("audio_gain", AUDIO_GAIN_PRECISION_VALUE)
        await session.commit()

        value = await repo.get("audio_gain")

        assert value == _approx_float(AUDIO_GAIN_PRECISION_VALUE), f"expected float precision to be maintained, got {value}"


@pytest.mark.integration
class TestPreferencesRepositoryCloudConsent:
    """Integration tests for cloud consent workflow - the primary use case."""

    async def test_cloud_consent_workflow_initial_state(self, session: AsyncSession) -> None:
        """Test cloud consent defaults to False when not set."""
        repo = SqlAlchemyPreferencesRepository(session)

        consent = await repo.get_bool("cloud_consent_granted", False)

        assert consent is False, f"expected cloud consent to default to False, got {consent}"

    async def test_cloud_consent_workflow_grant_consent(self, session: AsyncSession) -> None:
        """Test granting cloud consent persists correctly."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("cloud_consent_granted", True)
        await session.commit()

        consent = await repo.get_bool("cloud_consent_granted", False)

        assert consent is True, f"expected cloud consent to be granted, got {consent}"

    async def test_cloud_consent_workflow_revoke_consent(self, session: AsyncSession) -> None:
        """Test revoking cloud consent after granting."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("cloud_consent_granted", True)
        await session.commit()

        await repo.set("cloud_consent_granted", False)
        await session.commit()

        consent = await repo.get_bool("cloud_consent_granted", False)

        assert consent is False, f"expected cloud consent to be revoked, got {consent}"

    async def test_cloud_consent_survives_session_restart(
        self, session: AsyncSession
    ) -> None:
        """Test cloud consent persists across session boundaries.

        This simulates server restart by committing then reading in a fresh query.
        """
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("cloud_consent_granted", True)
        await session.commit()

        session.expire_all()

        consent = await repo.get_bool("cloud_consent_granted", False)

        assert consent is True, f"expected cloud consent to survive session restart, got {consent}"


@pytest.mark.integration
class TestPreferencesRepositoryBulkOperations:
    """Integration tests for bulk preference operations (Sprint 14)."""

    async def test_get_all_returns_all_preferences(self, session: AsyncSession) -> None:
        """get_all returns all stored preferences."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("key1", "value1")
        await repo.set("key2", "value2")
        await session.commit()

        result = await repo.get_all()

        assert "key1" in result, "should include first key"
        assert "key2" in result, "should include second key"
        assert result["key1"] == "value1", "should return first value"
        assert result["key2"] == "value2", "should return second value"

    async def test_get_all_with_key_filter(self, session: AsyncSession) -> None:
        """get_all filters by specified keys."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("include", "yes")
        await repo.set("exclude", "no")
        await session.commit()

        result = await repo.get_all(keys=["include"])

        assert "include" in result, "filtered key should be included in result"
        assert "exclude" not in result, "non-filtered key should not be in result"

    async def test_get_all_returns_empty_dict_when_no_prefs(
        self, session: AsyncSession
    ) -> None:
        """get_all returns empty dict when no preferences exist."""
        repo = SqlAlchemyPreferencesRepository(session)

        result = await repo.get_all()

        assert result == {}, f"expected empty dict when no preferences exist, got {result}"

    async def test_get_all_with_metadata_includes_timestamps(
        self, session: AsyncSession
    ) -> None:
        """get_all_with_metadata includes updated_at timestamps."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("theme", "dark")
        await session.commit()

        result = await repo.get_all_with_metadata()

        assert len(result) == 1, "should return one preference"
        assert result[0].key == "theme", "should have correct key"
        assert result[0].value == "dark", "should have correct value"
        assert result[0].updated_at is not None, "should include timestamp"

    async def test_get_max_updated_at_returns_most_recent(
        self, session: AsyncSession
    ) -> None:
        """get_max_updated_at returns the most recent timestamp."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("first", "value")
        await session.commit()
        await repo.set("second", "value")
        await session.commit()

        max_ts = await repo.get_max_updated_at()

        assert max_ts is not None, "expected max_updated_at to return a timestamp when preferences exist"

    async def test_get_max_updated_at_returns_none_when_empty(
        self, session: AsyncSession
    ) -> None:
        """get_max_updated_at returns None when no preferences exist."""
        repo = SqlAlchemyPreferencesRepository(session)

        max_ts = await repo.get_max_updated_at()

        assert max_ts is None, f"expected max_updated_at to return None when no preferences exist, got {max_ts}"

    async def test_set_bulk_creates_multiple_preferences(
        self, session: AsyncSession
    ) -> None:
        """set_bulk creates multiple preferences at once."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set_bulk({"key1": "value1", "key2": "value2", "key3": "value3"})
        await session.commit()

        result = await repo.get_all()

        assert len(result) == 3, f"expected 3 preferences from bulk set, got {len(result)}"
        assert result["key1"] == "value1", f"expected key1 to be 'value1', got {result.get('key1')!r}"
        assert result["key2"] == "value2", f"expected key2 to be 'value2', got {result.get('key2')!r}"
        assert result["key3"] == "value3", f"expected key3 to be 'value3', got {result.get('key3')!r}"

    async def test_set_bulk_updates_existing_preferences(
        self, session: AsyncSession
    ) -> None:
        """set_bulk updates existing preferences."""
        repo = SqlAlchemyPreferencesRepository(session)

        await repo.set("theme", "light")
        await session.commit()

        await repo.set_bulk({"theme": "dark", "new_key": "new_value"})
        await session.commit()

        result = await repo.get_all()

        assert result["theme"] == "dark", f"expected bulk set to update existing key, got {result.get('theme')!r}"
        assert result["new_key"] == "new_value", f"expected bulk set to create new key, got {result.get('new_key')!r}"
