"""Tests for metrics database module."""

from unittest.mock import MagicMock, Mock, patch

import pytest

from local_deep_research.metrics.database import MetricsDatabase, get_metrics_db


class TestMetricsDatabaseInit:
    """Tests for MetricsDatabase initialization."""

    def test_initializes_with_no_credentials(self):
        """Should initialize without credentials."""
        db = MetricsDatabase()
        assert db.username is None
        assert db.password is None

    def test_initializes_with_username(self):
        """Should store username if provided."""
        db = MetricsDatabase(username="testuser")
        assert db.username == "testuser"
        assert db.password is None

    def test_initializes_with_credentials(self):
        """Should store both username and password."""
        db = MetricsDatabase(username="testuser", password="testpass")
        assert db.username == "testuser"
        assert db.password == "testpass"


class TestMetricsDatabaseGetSession:
    """Tests for get_session method."""

    def test_yields_none_when_no_username_available(self):
        """Should yield None when no username available."""
        db = MetricsDatabase()

        with patch("flask.session", {}):
            with db.get_session() as session:
                assert session is None

    def test_uses_provided_username(self):
        """Should use username provided to get_session."""
        db = MetricsDatabase()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.metrics.database.get_user_db_session",
            return_value=mock_cm,
        ):
            with db.get_session(username="testuser") as session:
                assert session == mock_session

    def test_uses_stored_username_as_fallback(self):
        """Should use stored username if not provided."""
        db = MetricsDatabase(username="stored_user")
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.metrics.database.get_user_db_session",
            return_value=mock_cm,
        ):
            with db.get_session() as session:
                assert session == mock_session

    def test_uses_flask_session_as_fallback(self):
        """Should try Flask session if no username provided."""
        db = MetricsDatabase()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch("flask.session", {"username": "flask_user"}):
            with patch(
                "local_deep_research.metrics.database.get_user_db_session",
                return_value=mock_cm,
            ):
                with db.get_session() as session:
                    assert session == mock_session

    def test_uses_thread_safe_access_with_password(self):
        """Should use thread metrics writer when password provided."""
        db = MetricsDatabase(username="testuser", password="testpass")
        mock_session = MagicMock()
        mock_writer = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_writer.get_session.return_value = mock_cm

        with patch(
            "local_deep_research.database.thread_metrics.metrics_writer",
            mock_writer,
        ):
            with db.get_session() as session:
                mock_writer.set_user_password.assert_called_with(
                    "testuser", "testpass"
                )
                assert session == mock_session

    def test_overrides_stored_username(self):
        """Should use provided username over stored one."""
        db = MetricsDatabase(username="stored")
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.metrics.database.get_user_db_session",
            return_value=mock_cm,
        ) as mock_get_session:
            # Provide override username
            with db.get_session(username="override"):
                mock_get_session.assert_called_with("override")

    def test_handles_flask_import_error(self):
        """Should handle case when Flask is not available."""
        db = MetricsDatabase()

        # Simulate Flask not being available
        with patch.dict("sys.modules", {"flask": None}):
            with db.get_session() as session:
                # Should yield None without raising
                assert session is None


class TestGetMetricsDb:
    """Tests for get_metrics_db singleton function."""

    def test_returns_metrics_database_instance(self):
        """Should return a MetricsDatabase instance."""
        # Reset singleton for test
        import local_deep_research.metrics.database as db_module

        db_module._metrics_db = None

        result = get_metrics_db()
        assert isinstance(result, MetricsDatabase)

    def test_returns_same_instance_on_repeated_calls(self):
        """Should return the same instance (singleton)."""
        # Reset singleton for test
        import local_deep_research.metrics.database as db_module

        db_module._metrics_db = None

        first = get_metrics_db()
        second = get_metrics_db()

        assert first is second

    def test_singleton_persistence(self):
        """Singleton should persist across calls."""
        # Reset singleton for test
        import local_deep_research.metrics.database as db_module

        db_module._metrics_db = None

        db1 = get_metrics_db()
        db2 = get_metrics_db()
        db3 = get_metrics_db()

        assert db1 is db2 is db3


class TestMetricsDatabaseContextManager:
    """Tests for context manager behavior."""

    def test_context_manager_returns_session(self):
        """Context manager should yield a session."""
        db = MetricsDatabase(username="testuser")
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.metrics.database.get_user_db_session",
            return_value=mock_cm,
        ):
            with db.get_session() as session:
                assert session is not None

    def test_context_manager_handles_exceptions(self):
        """Context manager should handle exceptions properly."""
        db = MetricsDatabase(username="testuser")
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(side_effect=Exception("DB Error"))
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.metrics.database.get_user_db_session",
            return_value=mock_cm,
        ):
            with pytest.raises(Exception, match="DB Error"):
                with db.get_session():
                    pass


class TestFlaskSessionRuntimeError:
    """Tests for Flask session RuntimeError handling (PR #2014).

    PR #2014 changed bare `except:` to `except RuntimeError:` when
    accessing flask_session outside request context.
    """

    def test_runtime_error_caught_outside_flask_context(self):
        """RuntimeError is raised when accessing Flask session outside context."""
        with pytest.raises(RuntimeError):
            from flask import session as flask_session

            flask_session.get("username")

    def test_runtime_error_is_specific_exception(self):
        """RuntimeError is caught specifically, not by bare except."""
        caught_type = None
        try:
            from flask import session as flask_session

            flask_session.get("username")
        except RuntimeError:
            caught_type = "RuntimeError"
        except Exception:
            caught_type = "Exception"

        assert caught_type == "RuntimeError"
