import os
import sys
import tempfile
import types
import shutil
from pathlib import Path
from unittest.mock import Mock

import pytest
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import sessionmaker, Session

import src.local_deep_research.utilities.db_utils as db_utils_module
from src.local_deep_research.database.models import Base
from src.local_deep_research.database.auth_db import (
    init_auth_database,
)
from src.local_deep_research.web.app_factory import create_app
from src.local_deep_research.web.services.settings_manager import (
    SettingsManager,
)

# Import our mock fixtures
try:
    from .mock_fixtures import (
        get_mock_arxiv_response,
        get_mock_error_responses,
        get_mock_findings,
        get_mock_google_pse_response,
        get_mock_ollama_response,
        get_mock_pubmed_article,
        get_mock_pubmed_response,
        get_mock_research_history,
        get_mock_search_results,
        get_mock_semantic_scholar_response,
        get_mock_settings,
        get_mock_wikipedia_response,
    )
except ImportError:
    # Mock fixtures not yet created, skip for now
    pass


def pytest_configure(config):
    """Configure pytest with custom markers."""
    config.addinivalue_line(
        "markers",
        "requires_llm: mark test as requiring a real LLM (not fallback)",
    )

    # In CI, LDR_TESTING_WITH_MOCKS is set via Docker environment variables
    # For local testing, set it here if not already set
    if not os.environ.get("LDR_TESTING_WITH_MOCKS"):
        os.environ["LDR_TESTING_WITH_MOCKS"] = "true"


@pytest.fixture(autouse=True)
def skip_if_using_fallback_llm(request):
    """Skip tests marked with @pytest.mark.requires_llm when using fallback LLM."""
    if request.node.get_closest_marker("requires_llm"):
        if os.environ.get("LDR_USE_FALLBACK_LLM", ""):
            pytest.skip("Test requires real LLM but using fallback")


@pytest.fixture
def temp_data_dir():
    """Create a temporary data directory for testing."""
    temp_dir = tempfile.mkdtemp()
    yield Path(temp_dir)
    shutil.rmtree(temp_dir)


@pytest.fixture(autouse=True)
def cleanup_database_connections():
    """Clean up database connections before and after each test."""
    # Import here to avoid circular imports
    from src.local_deep_research.database.encrypted_db import db_manager
    from src.local_deep_research.web.auth.routes import session_manager

    # Clear connections and sessions before test
    db_manager.connections.clear()
    session_manager.sessions.clear()

    yield

    # Close any open connections
    for username, engine in list(db_manager.connections.items()):
        try:
            engine.dispose()
        except Exception:
            pass

    # Clear connections and sessions after test
    db_manager.connections.clear()
    session_manager.sessions.clear()


@pytest.fixture
def app(temp_data_dir, monkeypatch):
    """Create a Flask app configured for testing."""
    # Override data directory
    monkeypatch.setenv("LDR_DATA_DIR", str(temp_data_dir))

    # Set testing environment
    monkeypatch.setenv("TESTING", "1")
    # Use fallback LLM for tests
    monkeypatch.setenv("LDR_USE_FALLBACK_LLM", "1")

    # Create app with testing config
    app, _ = create_app()
    app.config["TESTING"] = True
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["WTF_CSRF_CHECK_DEFAULT"] = False
    app.config["SESSION_COOKIE_SECURE"] = False  # For testing without HTTPS

    # Initialize auth database in test directory
    init_auth_database()

    return app


@pytest.fixture
def client(app):
    """Create a test client."""
    return app.test_client()


@pytest.fixture
def authenticated_client(app, temp_data_dir):
    """Create a test client with an authenticated user."""
    # Import here to avoid circular imports
    import time

    # Create unique test username to avoid conflicts
    test_username = f"pytest_user_{int(time.time() * 1000)}"
    test_password = "testpassword123"

    # Clear any existing user database
    encrypted_db_dir = temp_data_dir / "encrypted_databases"
    if encrypted_db_dir.exists():
        import shutil

        try:
            shutil.rmtree(encrypted_db_dir)
        except Exception as e:
            print(f"Warning: Could not remove encrypted_db_dir: {e}")

    # Create a test client
    client = app.test_client()

    # Register and login the user through the normal flow
    with client:
        # Register new unique user
        register_response = client.post(
            "/auth/register",
            data={
                "username": test_username,
                "password": test_password,
                "confirm_password": test_password,
                "acknowledge": "true",
            },
            follow_redirects=False,
        )

        if register_response.status_code not in [200, 302]:
            print(
                f"Registration failed with status {register_response.status_code}"
            )
            print(f"Response: {register_response.data.decode()[:500]}")
            raise Exception(
                f"Registration failed with status {register_response.status_code}"
            )

        # Login user
        login_response = client.post(
            "/auth/login",
            data={"username": test_username, "password": test_password},
            follow_redirects=False,
        )

        if login_response.status_code not in [200, 302]:
            print(f"Login failed with status {login_response.status_code}")
            print(f"Response: {login_response.data.decode()[:500]}")
            raise Exception(
                f"Login failed with status {login_response.status_code}"
            )

    return client


@pytest.fixture()
def setup_database_for_all_tests(
    tmp_path_factory, session_mocker
):  # Directly use the session_mocker provided by pytest-mock
    """
    Provides a database setup for a temporary SQLite file database for the entire test session.
    It patches db_utils.get_db_session and db_utils.get_settings_manager to use this test DB.
    """

    # Call cache_clear on the functions from db_utils_module.
    # This ensures any pre-existing cached instances are gone.
    # We must ensure db_utils_module is imported before this point.
    try:
        if hasattr(db_utils_module.get_db_session, "cache_clear"):
            db_utils_module.get_db_session.cache_clear()
        if hasattr(db_utils_module.get_settings_manager, "cache_clear"):
            db_utils_module.get_settings_manager.cache_clear()
        # get_setting_from_db_main_thread has been removed

    except Exception as e:
        print(f"ERROR: Failed to clear db_utils caches aggressively: {e}")
        # This shouldn't prevent test run, but indicates a problem with cache_clear

    # Debug tmp_path_factory behavior
    temp_dir = tmp_path_factory.mktemp("db_test_data")
    db_file = temp_dir / "test_settings.db"
    db_url = f"sqlite:///{db_file}"

    engine = None
    try:
        engine = create_engine(db_url)
    except Exception as e:
        print(f"ERROR: Failed to create SQLAlchemy engine: {e}")
        raise

    try:
        Base.metadata.create_all(engine)
    except SQLAlchemyError as e:
        print(f"ERROR: SQLAlchemyError during Base.metadata.create_all: {e}")
        raise
    except Exception as e:
        print(f"ERROR: Unexpected error during Base.metadata.create_all: {e}")
        raise

    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    temp_session = SessionLocal()
    temp_settings_manager = SettingsManager(db_session=temp_session)

    try:
        temp_settings_manager.load_from_defaults_file(commit=True)
    except Exception as e:
        print(f"ERROR: Failed to load default settings: {e}")
        temp_session.rollback()  # Rollback if default loading fails
        raise  # Re-raise to fail the test if default loading is critical
    finally:
        temp_session.close()  # Close the temporary session used for loading defaults

    # Clear caches and patch
    db_utils_module.get_db_session.cache_clear()

    mock_get_db_session = session_mocker.patch(
        "src.local_deep_research.utilities.db_utils.get_db_session"
    )
    mock_get_db_session.side_effect = SessionLocal

    mock_get_settings_manager = session_mocker.patch(
        "src.local_deep_research.utilities.db_utils.get_settings_manager"
    )

    def _settings_with_maybe_fake_db(
        db_session: Session | None = None, *_, **__
    ) -> SettingsManager:
        if db_session is None:
            # Use the mock.
            db_session = mock_get_db_session()
        return SettingsManager(db_session=db_session)

    mock_get_settings_manager.side_effect = _settings_with_maybe_fake_db

    yield SessionLocal  # Yield the SessionLocal class for individual tests to create sessions

    if engine:
        engine.dispose()  # Dispose the engine to close all connections
    # tmp_path_factory handles deleting the temporary directory and its contents


@pytest.fixture
def mock_db_session(mocker):
    return mocker.MagicMock()


@pytest.fixture
def mock_logger(mocker):
    return mocker.patch(
        "src.local_deep_research.web.services.settings_manager.logger"
    )


# ============== LLM and Search Mock Fixtures (inspired by scottvr) ==============


@pytest.fixture
def mock_llm():
    """Create a mock LLM for testing."""
    mock = Mock()
    mock.invoke.return_value = Mock(content="Mocked LLM response")
    return mock


@pytest.fixture
def mock_search():
    """Create a mock search engine for testing."""
    mock = Mock()
    mock.run.return_value = get_mock_search_results()
    return mock


@pytest.fixture
def mock_search_system():
    """Create a mock search system for testing."""
    mock = Mock()
    mock.analyze_topic.return_value = get_mock_findings()
    mock.all_links_of_system = [
        {"title": "Source 1", "link": "https://example.com/1"},
        {"title": "Source 2", "link": "https://example.com/2"},
    ]
    return mock


# ============== API Response Mock Fixtures ==============


@pytest.fixture
def mock_wikipedia_response():
    """Mock response from Wikipedia API."""
    return get_mock_wikipedia_response()


@pytest.fixture
def mock_arxiv_response():
    """Mock response from arXiv API."""
    return get_mock_arxiv_response()


@pytest.fixture
def mock_pubmed_response():
    """Mock response from PubMed API."""
    return get_mock_pubmed_response()


@pytest.fixture
def mock_pubmed_article():
    """Mock PubMed article detail."""
    return get_mock_pubmed_article()


@pytest.fixture
def mock_semantic_scholar_response():
    """Mock response from Semantic Scholar API."""
    return get_mock_semantic_scholar_response()


@pytest.fixture
def mock_google_pse_response():
    """Mock response from Google PSE API."""
    return get_mock_google_pse_response()


@pytest.fixture
def mock_ollama_response():
    """Mock response from Ollama API."""
    return get_mock_ollama_response()


# ============== Data Structure Mock Fixtures ==============


@pytest.fixture
def mock_search_results():
    """Sample search results for testing."""
    return get_mock_search_results()


@pytest.fixture
def mock_findings():
    """Sample research findings for testing."""
    return get_mock_findings()


@pytest.fixture
def mock_error_responses():
    """Collection of error responses for testing."""
    return get_mock_error_responses()


# ============== Environment and Module Mock Fixtures ==============


@pytest.fixture
def mock_env_vars(monkeypatch):
    """Set up mock environment variables for testing."""
    monkeypatch.setenv("LDR_LLM__PROVIDER", "test_provider")
    monkeypatch.setenv("LDR_LLM__MODEL", "test_model")
    monkeypatch.setenv("LDR_SEARCH__TOOL", "test_tool")
    monkeypatch.setenv("LDR_SEARCH__ITERATIONS", "2")
    yield


@pytest.fixture
def mock_llm_config(monkeypatch):
    """Create and patch a mock llm_config module."""
    # Create a mock module
    mock_module = types.ModuleType("mock_llm_config")

    # Add necessary functions and variables
    def get_llm(*args, **kwargs):
        mock = Mock()
        mock.invoke.return_value = Mock(content="Mocked LLM response")
        return mock

    mock_module.get_llm = get_llm
    mock_module.VALID_PROVIDERS = [
        "ollama",
        "openai",
        "anthropic",
        "vllm",
        "openai_endpoint",
        "lmstudio",
        "llamacpp",
        "none",
    ]
    mock_module.AVAILABLE_PROVIDERS = {"ollama": "Ollama (local models)"}
    mock_module.get_available_providers = (
        lambda: mock_module.AVAILABLE_PROVIDERS
    )

    # Patch the module
    monkeypatch.setitem(
        sys.modules, "src.local_deep_research.config.llm_config", mock_module
    )
    monkeypatch.setattr(
        "src.local_deep_research.config.llm_config", mock_module
    )

    return mock_module


# ============== Test Database Fixtures ==============


@pytest.fixture
def temp_db_path():
    """Create a temporary database file for testing."""
    fd, path = tempfile.mkstemp(suffix=".db")
    os.close(fd)
    yield path
    os.unlink(path)


@pytest.fixture
def mock_research_history():
    """Mock research history entries."""
    return get_mock_research_history()


@pytest.fixture
def mock_settings():
    """Mock settings configuration."""
    return get_mock_settings()
