"""Unit tests for integrations/state.py module.

Tests StateManager abstract base class, SQLiteStateManager with file and in-memory
persistence, database corruption recovery, and InMemoryStateManager.
"""

import pytest
import tempfile
import sqlite3
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock

from mca_sdk.integrations.state import (
    StateManager,
    SQLiteStateManager,
    InMemoryStateManager,
)


class TestStateManagerABC:
    """Test StateManager abstract base class."""

    def test_cannot_instantiate_abstract_class(self):
        """Test that StateManager cannot be instantiated directly."""
        with pytest.raises(TypeError, match="Can't instantiate abstract class"):
            StateManager()

    def test_subclass_must_implement_all_methods(self):
        """Test that subclass must implement all abstract methods."""

        class IncompleteStateManager(StateManager):
            def connect(self):
                pass

        with pytest.raises(TypeError, match="Can't instantiate abstract class"):
            IncompleteStateManager()


class TestSQLiteStateManagerInit:
    """Test SQLiteStateManager initialization."""

    def test_init_with_file_path(self):
        """Test initialization with file path."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)

            assert manager.db_path == Path(db_path)
            assert manager._conn is None

    def test_init_with_none_path_uses_memory(self):
        """Test that None path creates in-memory database."""
        manager = SQLiteStateManager(None)

        assert manager.db_path == ":memory:"
        assert manager._conn is None

    def test_init_logs_file_path(self):
        """Test that initialization logs the file path."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")

            with patch("mca_sdk.integrations.state.logger") as mock_logger:
                manager = SQLiteStateManager(db_path)

                mock_logger.info.assert_called_once()
                assert "SQLite state persistence" in str(mock_logger.info.call_args)

    def test_init_logs_memory_usage(self):
        """Test that initialization logs in-memory usage."""
        with patch("mca_sdk.integrations.state.logger") as mock_logger:
            manager = SQLiteStateManager(None)

            mock_logger.info.assert_called_once()
            assert "in-memory" in str(mock_logger.info.call_args).lower()


class TestSQLiteStateManagerConnect:
    """Test SQLiteStateManager.connect() method."""

    def test_connect_creates_database_file(self):
        """Test that connect() creates database file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)

            manager.connect()

            assert Path(db_path).exists()
            manager.close()

    def test_connect_creates_parent_directories(self):
        """Test that connect() creates parent directories if needed."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "subdir" / "nested" / "test.db")
            manager = SQLiteStateManager(db_path)

            manager.connect()

            assert Path(db_path).parent.exists()
            assert Path(db_path).exists()
            manager.close()

    def test_connect_creates_table(self):
        """Test that connect() creates state table."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)

            manager.connect()

            # Check table exists
            cursor = manager._conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='state'")
            assert cursor.fetchone() is not None
            manager.close()

    def test_connect_enables_wal_mode(self):
        """Test that connect() enables WAL mode for better concurrency."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)

            manager.connect()

            # Check WAL mode is enabled
            cursor = manager._conn.cursor()
            cursor.execute("PRAGMA journal_mode")
            result = cursor.fetchone()
            assert result[0].upper() == "WAL"
            manager.close()

    def test_connect_with_memory_database(self):
        """Test connect() with in-memory database."""
        manager = SQLiteStateManager(None)

        manager.connect()

        assert manager._conn is not None
        manager.close()

    def test_connect_handles_corrupted_database(self):
        """Test that connect() handles corrupted database by recreating."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "test.db"

            # Create corrupted database file
            with open(db_path, "wb") as f:
                f.write(b"not a valid sqlite database file")

            manager = SQLiteStateManager(str(db_path))

            # Should delete corrupted file and recreate
            manager.connect()

            # Should now be a valid database
            assert manager._conn is not None
            cursor = manager._conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='state'")
            assert cursor.fetchone() is not None
            manager.close()

    def test_connect_raises_on_persistent_error(self):
        """Test that connect() raises error if database cannot be created."""
        # Use an invalid path that will fail sqlite3.connect
        db_path = "/proc/invalid/path/test.db"  # /proc is read-only

        manager = SQLiteStateManager(db_path)

        with pytest.raises((sqlite3.Error, OSError)):
            manager.connect()


class TestSQLiteStateManagerLoadState:
    """Test SQLiteStateManager.load_state() method."""

    def test_load_state_returns_empty_dict_for_new_database(self):
        """Test that load_state() returns empty dict for new database."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            state = manager.load_state()

            assert state == {}
            manager.close()

    def test_load_state_returns_saved_data(self):
        """Test that load_state() returns previously saved data."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            # Insert data directly
            with manager._conn:
                manager._conn.execute(
                    "INSERT INTO state (key, value) VALUES (?, ?)", ("predictions", 100)
                )
                manager._conn.execute("INSERT INTO state (key, value) VALUES (?, ?)", ("errors", 5))

            state = manager.load_state()

            assert state == {"predictions": 100, "errors": 5}
            manager.close()

    def test_load_state_auto_connects_if_not_connected(self):
        """Test that load_state() auto-connects if not connected."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)

            # Don't call connect() first
            state = manager.load_state()

            assert state == {}
            assert manager._conn is not None
            manager.close()

    def test_load_state_handles_error_gracefully(self):
        """Test that load_state() returns empty dict on error."""
        manager = SQLiteStateManager(None)
        manager.connect()

        # Close connection to simulate error
        manager._conn.close()

        # Create invalid connection state
        manager._conn = MagicMock()
        manager._conn.__enter__ = MagicMock(side_effect=sqlite3.Error("Mock error"))

        state = manager.load_state()

        assert state == {}


class TestSQLiteStateManagerSaveState:
    """Test SQLiteStateManager.save_state() method."""

    def test_save_state_persists_data(self):
        """Test that save_state() persists data to database."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            manager.save_state({"predictions": 100, "errors": 5})

            # Verify data persisted
            cursor = manager._conn.cursor()
            cursor.execute("SELECT key, value FROM state ORDER BY key")
            rows = cursor.fetchall()
            assert rows == [("errors", 5), ("predictions", 100)]
            manager.close()

    def test_save_state_updates_existing_data(self):
        """Test that save_state() updates existing data."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            # Save initial state
            manager.save_state({"predictions": 100})

            # Update state
            manager.save_state({"predictions": 200})

            # Verify updated
            state = manager.load_state()
            assert state == {"predictions": 200}
            manager.close()

    def test_save_state_handles_no_connection(self):
        """Test that save_state() handles missing connection gracefully."""
        manager = SQLiteStateManager(None)
        # Don't call connect()

        # Should not raise, just log error
        manager.save_state({"predictions": 100})

    def test_save_state_handles_error(self):
        """Test that save_state() handles database errors."""
        manager = SQLiteStateManager(None)
        manager.connect()

        # Close connection to simulate error
        manager._conn.close()

        # Should not raise, just log error
        manager.save_state({"predictions": 100})


class TestSQLiteStateManagerClose:
    """Test SQLiteStateManager.close() method."""

    def test_close_closes_connection(self):
        """Test that close() closes database connection."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            manager.close()

            assert manager._conn is None

    def test_close_when_not_connected(self):
        """Test that close() works when not connected."""
        manager = SQLiteStateManager(None)

        # Should not raise
        manager.close()

    def test_close_sets_connection_to_none(self):
        """Test that close() sets _conn to None."""
        manager = SQLiteStateManager(None)
        manager.connect()

        manager.close()

        assert manager._conn is None


class TestSQLiteStateManagerEndToEnd:
    """Test SQLiteStateManager end-to-end workflows."""

    def test_full_lifecycle(self):
        """Test complete lifecycle: connect, save, load, close."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")

            # Create and save state
            manager = SQLiteStateManager(db_path)
            manager.connect()
            manager.save_state({"predictions": 100, "errors": 5})
            manager.close()

            # Load state in new instance
            manager2 = SQLiteStateManager(db_path)
            manager2.connect()
            state = manager2.load_state()
            assert state == {"predictions": 100, "errors": 5}
            manager2.close()

    def test_cumulative_updates(self):
        """Test cumulative metric updates across saves."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = str(Path(tmpdir) / "test.db")
            manager = SQLiteStateManager(db_path)
            manager.connect()

            # Simulate cumulative updates
            state = manager.load_state()
            state["predictions"] = state.get("predictions", 0) + 50
            manager.save_state(state)

            state = manager.load_state()
            state["predictions"] = state.get("predictions", 0) + 50
            manager.save_state(state)

            final_state = manager.load_state()
            assert final_state["predictions"] == 100
            manager.close()


class TestInMemoryStateManager:
    """Test InMemoryStateManager."""

    def test_init(self):
        """Test initialization."""
        manager = InMemoryStateManager()

        assert manager._state == {}

    def test_connect_is_noop(self):
        """Test that connect() is a no-op."""
        manager = InMemoryStateManager()

        # Should not raise
        manager.connect()

    def test_load_state_returns_copy(self):
        """Test that load_state() returns a copy."""
        manager = InMemoryStateManager()
        manager._state["predictions"] = 100

        state1 = manager.load_state()
        state2 = manager.load_state()

        # Should be equal but not same object
        assert state1 == state2
        assert state1 is not state2

    def test_save_state_updates_internal_state(self):
        """Test that save_state() updates internal state."""
        manager = InMemoryStateManager()

        manager.save_state({"predictions": 100, "errors": 5})

        state = manager.load_state()
        assert state == {"predictions": 100, "errors": 5}

    def test_save_state_merges_updates(self):
        """Test that save_state() merges updates."""
        manager = InMemoryStateManager()

        manager.save_state({"predictions": 100})
        manager.save_state({"errors": 5})

        state = manager.load_state()
        assert state == {"predictions": 100, "errors": 5}

    def test_close_is_noop(self):
        """Test that close() is a no-op."""
        manager = InMemoryStateManager()

        # Should not raise
        manager.close()

    def test_state_not_persisted_across_instances(self):
        """Test that state is not persisted across instances."""
        manager1 = InMemoryStateManager()
        manager1.save_state({"predictions": 100})

        manager2 = InMemoryStateManager()
        state = manager2.load_state()

        # New instance should have empty state
        assert state == {}

    def test_full_lifecycle(self):
        """Test complete lifecycle for in-memory manager."""
        manager = InMemoryStateManager()

        manager.connect()
        manager.save_state({"predictions": 100})
        state = manager.load_state()
        assert state == {"predictions": 100}
        manager.close()
