"""Tests for database connection and initialization.

Tests open_database, init_database, get_schema_version from db/connection.py.
Uses tmp_path fixture for file-based tests, in-memory for pure unit tests.
"""

from __future__ import annotations

import sqlite3
from pathlib import Path

import pytest

import unfamiliar.db.connection as conn_module
from unfamiliar.db import (
    CampaignRepository,
    ChainRepository,
    FileSnapshotRepository,
    MissionRepository,
    ModuleRepository,
    open_campaign_db,
)
from unfamiliar.db.connection import (
    DatabaseError,
    get_schema_version,
    init_database,
    open_database,
)


class TestOpenDatabase:
    """Tests for open_database function."""

    def test_creates_file_at_path(self, tmp_path: Path):
        """open_database creates a new database file at specified path."""
        db_path = tmp_path / "test.db"
        assert not db_path.exists()

        conn = open_database(db_path)

        assert db_path.exists()
        assert isinstance(conn, sqlite3.Connection)
        conn.close()

    def test_enables_wal_mode(self, tmp_path: Path):
        """open_database enables WAL journal mode."""
        db_path = tmp_path / "test.db"
        conn = open_database(db_path)

        result = conn.execute("PRAGMA journal_mode").fetchone()[0]
        assert result.lower() == "wal"
        conn.close()

    def test_enables_foreign_keys(self, tmp_path: Path):
        """open_database enables foreign key constraints."""
        db_path = tmp_path / "test.db"
        conn = open_database(db_path)

        result = conn.execute("PRAGMA foreign_keys").fetchone()[0]
        assert result == 1
        conn.close()

    def test_sets_row_factory_to_row(self, tmp_path: Path):
        """open_database sets row_factory to sqlite3.Row."""
        db_path = tmp_path / "test.db"
        conn = open_database(db_path)

        assert conn.row_factory == sqlite3.Row
        conn.close()

    def test_creates_parent_directories(self, tmp_path: Path):
        """open_database creates parent directories if they don't exist."""
        db_path = tmp_path / "subdir" / "nested" / "test.db"
        assert not db_path.parent.exists()

        conn = open_database(db_path)

        assert db_path.parent.exists()
        assert db_path.exists()
        conn.close()

    def test_opens_existing_database(self, tmp_path: Path):
        """open_database opens existing database without error."""
        db_path = tmp_path / "test.db"
        conn1 = open_database(db_path)
        conn1.close()

        conn2 = open_database(db_path)
        assert isinstance(conn2, sqlite3.Connection)
        conn2.close()

    def test_raises_database_error_when_mkdir_fails(self, tmp_path: Path, monkeypatch):
        """open_database wraps OSError from mkdir in DatabaseError."""
        db_path = tmp_path / "subdir" / "test.db"

        def raise_oserror(*args, **kwargs):
            raise OSError("Permission denied")

        monkeypatch.setattr(Path, "mkdir", raise_oserror)

        with pytest.raises(DatabaseError) as exc_info:
            open_database(db_path)

        assert "Cannot create database directory" in str(exc_info.value)
        assert isinstance(exc_info.value.__cause__, OSError)

    def test_raises_database_error_when_connect_fails(
        self, tmp_path: Path, monkeypatch
    ):
        """open_database wraps sqlite3.Error from connect in DatabaseError."""
        db_path = tmp_path / "test.db"

        def raise_sqlite_error(*args, **kwargs):
            raise sqlite3.Error("unable to open database file")

        monkeypatch.setattr(sqlite3, "connect", raise_sqlite_error)

        with pytest.raises(DatabaseError) as exc_info:
            open_database(db_path)

        assert "Cannot open database" in str(exc_info.value)
        assert isinstance(exc_info.value.__cause__, sqlite3.Error)


class TestInitDatabase:
    """Tests for init_database function."""

    def test_creates_schema_tables(self, db_conn: sqlite3.Connection):
        """init_database creates all expected tables."""
        cursor = db_conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
        )
        tables = {row[0] for row in cursor.fetchall()}

        expected = {
            "meta",
            "campaign",
            "briefing",
            "module",
            "mission",
            "mission_file_ref",
            "mission_prerequisite",
            "module_mission",
            "mission_chain",
            "chain_mission",
            "file_snapshot",
        }
        # sqlite_sequence is auto-created by SQLite for AUTOINCREMENT columns
        tables.discard("sqlite_sequence")
        assert tables == expected

    def test_sets_schema_version(self, db_conn: sqlite3.Connection):
        """init_database sets schema_version in meta table."""
        version = get_schema_version(db_conn)
        assert version == "1.0"

    def test_idempotent_double_init(self, db_conn: sqlite3.Connection):
        """Calling init_database twice leaves DB in valid state."""
        # First init already done by fixture
        init_database(db_conn)  # Second call should be safe

        version = get_schema_version(db_conn)
        assert version == "1.0"

        # All tables still exist
        cursor = db_conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
        )
        tables = {row[0] for row in cursor.fetchall()}
        tables.discard("sqlite_sequence")
        assert "campaign" in tables
        assert "mission" in tables
        assert "meta" in tables

        # No duplicate meta entries
        cursor = db_conn.execute(
            "SELECT COUNT(*) FROM meta WHERE key = 'schema_version'"
        )
        assert cursor.fetchone()[0] == 1

    def test_meta_has_exactly_one_schema_version_row(self, db_conn: sqlite3.Connection):
        """After double-init, meta table has exactly one schema_version row."""
        init_database(db_conn)  # Second call

        cursor = db_conn.execute(
            "SELECT COUNT(*) FROM meta WHERE key = 'schema_version'"
        )
        count = cursor.fetchone()[0]
        assert count == 1

    def test_raises_database_error_when_schema_fails(self, monkeypatch):
        """init_database wraps sqlite3.Error from executescript in DatabaseError."""
        conn = sqlite3.connect(":memory:")

        monkeypatch.setattr(conn_module, "SCHEMA_SQL", "INVALID SQL SYNTAX !!!")

        with pytest.raises(DatabaseError) as exc_info:
            init_database(conn)

        assert "Failed to initialize database schema" in str(exc_info.value)
        assert isinstance(exc_info.value.__cause__, sqlite3.Error)

        conn.close()


class TestGetSchemaVersion:
    """Tests for get_schema_version function."""

    def test_returns_none_for_fresh_connection(self):
        """get_schema_version returns None for uninitialized database."""
        conn = sqlite3.connect(":memory:")
        version = get_schema_version(conn)
        assert version is None
        conn.close()

    def test_returns_version_after_init(self, db_conn: sqlite3.Connection):
        """get_schema_version returns version string after init_database."""
        version = get_schema_version(db_conn)
        assert version == "1.0"
        assert isinstance(version, str)

    def test_returns_none_for_connection_without_meta_table(self):
        """get_schema_version returns None if meta table doesn't exist."""
        conn = sqlite3.connect(":memory:")
        conn.execute("CREATE TABLE other (id INTEGER)")
        version = get_schema_version(conn)
        assert version is None
        conn.close()


class TestDatabaseError:
    """Tests for DatabaseError exception."""

    def test_is_exception_subclass(self):
        """DatabaseError is a subclass of Exception."""
        assert issubclass(DatabaseError, Exception)

    def test_can_be_raised_with_message(self):
        """DatabaseError can be raised with a message."""
        with pytest.raises(DatabaseError) as exc_info:
            raise DatabaseError("Test error message")

        assert str(exc_info.value) == "Test error message"


class TestOpenCampaignDb:
    """Tests for open_campaign_db function."""

    def test_returns_all_repositories(self, tmp_path: Path):
        """open_campaign_db returns connection and all 5 repositories."""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        result = open_campaign_db(campaign_dir)
        conn = result.conn
        campaign_repo = result.campaign_repo
        module_repo = result.module_repo
        mission_repo = result.mission_repo
        chain_repo = result.chain_repo
        snapshot_repo = result.snapshot_repo

        # Verify connection
        assert isinstance(conn, sqlite3.Connection)

        # Verify all repositories are correct types
        assert isinstance(campaign_repo, CampaignRepository)
        assert isinstance(module_repo, ModuleRepository)
        assert isinstance(mission_repo, MissionRepository)
        assert isinstance(chain_repo, ChainRepository)
        assert isinstance(snapshot_repo, FileSnapshotRepository)

        conn.close()

    def test_enables_foreign_keys(self, tmp_path: Path):
        """open_campaign_db connection has foreign keys enabled."""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        db = open_campaign_db(campaign_dir)
        conn = db.conn

        result = conn.execute("PRAGMA foreign_keys").fetchone()[0]
        assert result == 1
        conn.close()

    def test_sets_schema_version(self, tmp_path: Path):
        """open_campaign_db initializes schema with version."""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        db = open_campaign_db(campaign_dir)
        conn = db.conn

        version = conn.execute(
            "SELECT value FROM meta WHERE key = 'schema_version'"
        ).fetchone()[0]
        assert version == "1.0"
        conn.close()

    def test_creates_database_file(self, tmp_path: Path):
        """open_campaign_db creates campaign.db in campaign directory."""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        db = open_campaign_db(campaign_dir)
        conn = db.conn

        db_path = campaign_dir / "campaign.db"
        assert db_path.exists()
        conn.close()

    def test_creates_repo_path_marker(self, tmp_path: Path):
        """open_campaign_db creates .repo_path marker when repo_path provided."""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        db = open_campaign_db(campaign_dir, repo_path="/path/to/repo")
        conn = db.conn

        marker = campaign_dir / ".repo_path"
        assert marker.exists()
        assert marker.read_text() == "/path/to/repo"
        conn.close()

    def test_context_manager_closes_connection(self, tmp_path: Path):
        """CampaignDB closes connection on context exit"""
        campaign_dir = tmp_path / "campaign"
        campaign_dir.mkdir()

        with open_campaign_db(campaign_dir) as db:
            conn = db.conn
            #             Connection should be open
            assert conn.execute("SELECT 1").fetchone()[0] == 1

        #         Connection should be closed after exiting context
        with pytest.raises(sqlite3.ProgrammingError):
            conn.execute("SELECT 1")
