"""
tests/test_db_permissions.py — Tests for SQLiteAdapter DB file permission security.

Issue #95: SQLiteAdapter must create DB files with 0o600 (owner read/write only)
instead of default OS permissions.
"""
from __future__ import annotations

import os
import stat
import tempfile
from pathlib import Path

import pytest
import pytest_asyncio

from cortex.storage import SQLiteAdapter


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def file_mode(path: str | Path) -> int:
    """Return the permission bits (lower 9 bits) of a file."""
    return stat.S_IMODE(os.stat(str(path)).st_mode)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestDBFilePermissions:
    """SQLiteAdapter must apply restrictive permissions to on-disk DB files."""

    @pytest.mark.asyncio
    async def test_new_file_has_0o600_permissions(self, tmp_path):
        """A freshly-created DB file must have mode 0o600."""
        db_file = tmp_path / "cortex.db"
        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        await adapter.initialize()
        await adapter.close()

        assert db_file.exists(), "DB file was not created"
        assert file_mode(db_file) == 0o600, (
            f"Expected 0o600, got 0o{file_mode(db_file):03o}"
        )

    @pytest.mark.asyncio
    async def test_existing_file_permissions_tightened(self, tmp_path):
        """If the DB file already exists with looser permissions, initialize()
        must tighten them to db_file_mode."""
        db_file = tmp_path / "cortex_loose.db"

        # Pre-create with loose permissions (simulate a pre-existing insecure file)
        db_file.touch()
        os.chmod(str(db_file), 0o644)
        assert file_mode(db_file) == 0o644, "Pre-condition: file should start with 0o644"

        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        await adapter.initialize()
        await adapter.close()

        assert file_mode(db_file) == 0o600, (
            f"Expected permissions to be tightened to 0o600, got 0o{file_mode(db_file):03o}"
        )

    @pytest.mark.asyncio
    async def test_custom_db_file_mode_respected(self, tmp_path):
        """The db_file_mode constructor arg must override the default 0o600."""
        db_file = tmp_path / "cortex_custom.db"
        adapter = SQLiteAdapter(
            db_path=str(db_file),
            enable_wal=False,
            db_file_mode=0o640,
        )
        await adapter.initialize()
        await adapter.close()

        assert file_mode(db_file) == 0o640, (
            f"Expected 0o640, got 0o{file_mode(db_file):03o}"
        )

    @pytest.mark.asyncio
    async def test_memory_db_skips_permission_changes(self):
        """:memory: databases must be fully functional without any permission ops."""
        adapter = SQLiteAdapter(db_path=":memory:", enable_wal=False)
        # Should not raise any exception (no file to chmod)
        await adapter.initialize()
        assert await adapter.ping(), ":memory: adapter should be pingable"
        await adapter.close()

    @pytest.mark.asyncio
    async def test_default_db_file_mode_constant(self):
        """DEFAULT_DB_FILE_MODE class constant must be 0o600."""
        assert SQLiteAdapter.DEFAULT_DB_FILE_MODE == 0o600

    @pytest.mark.asyncio
    async def test_umask_restored_after_initialize(self, tmp_path):
        """The process umask must be restored to its original value after initialize()."""
        db_file = tmp_path / "cortex_umask.db"
        original_umask = os.umask(0o022)
        os.umask(original_umask)  # restore immediately — just capturing it

        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        await adapter.initialize()
        await adapter.close()

        restored_umask = os.umask(original_umask)
        os.umask(original_umask)  # restore again
        assert restored_umask == original_umask, (
            f"Umask was not restored: expected 0o{original_umask:03o}, "
            f"got 0o{restored_umask:03o}"
        )

    @pytest.mark.asyncio
    async def test_db_is_functional_after_permission_set(self, tmp_path):
        """After setting permissions, the adapter must be fully operational."""
        db_file = tmp_path / "cortex_functional.db"
        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        await adapter.initialize()

        # Verify basic operations work
        assert await adapter.ping()

        entity = await adapter.create_entity(
            canonical_name="TestEntity",
            entity_type="test",
            owner_id="test-owner",
        )
        assert entity.canonical_name == "TestEntity"

        fetched = await adapter.get_entity(entity.id)
        assert fetched is not None
        assert fetched.id == entity.id

        await adapter.close()

    @pytest.mark.asyncio
    async def test_file_not_world_readable(self, tmp_path):
        """DB file must not be readable by others (world-readable bit off)."""
        db_file = tmp_path / "cortex_noworld.db"
        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        await adapter.initialize()
        await adapter.close()

        mode = file_mode(db_file)
        assert not (mode & stat.S_IROTH), (
            f"DB file should not be world-readable, got mode 0o{mode:03o}"
        )
        assert not (mode & stat.S_IWOTH), (
            f"DB file should not be world-writable, got mode 0o{mode:03o}"
        )
        assert not (mode & stat.S_IRGRP), (
            f"DB file should not be group-readable, got mode 0o{mode:03o}"
        )
        assert not (mode & stat.S_IWGRP), (
            f"DB file should not be group-writable, got mode 0o{mode:03o}"
        )


class TestReadOnlyFilesystem:
    """SQLiteAdapter must raise PermissionError on read-only filesystems (#396)."""

    @pytest.mark.asyncio
    async def test_readonly_db_file_raises(self, tmp_path):
        """Existing read-only DB file must raise PermissionError at init."""
        db_file = tmp_path / "readonly.db"
        db_file.touch()
        os.chmod(str(db_file), stat.S_IRUSR)

        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        with pytest.raises(PermissionError, match="not writable"):
            await adapter.initialize()

        os.chmod(str(db_file), stat.S_IRUSR | stat.S_IWUSR)

    @pytest.mark.asyncio
    async def test_readonly_directory_raises(self, tmp_path):
        """Read-only directory must raise PermissionError at init."""
        ro_dir = tmp_path / "readonly_dir"
        ro_dir.mkdir()
        os.chmod(str(ro_dir), stat.S_IRUSR | stat.S_IXUSR)

        db_file = ro_dir / "cortex.db"
        adapter = SQLiteAdapter(db_path=str(db_file), enable_wal=False)
        with pytest.raises(PermissionError, match="not writable"):
            await adapter.initialize()

        os.chmod(str(ro_dir), stat.S_IRWXU)
