"""Tests for KeyringKeyStore and InMemoryKeyStore."""

from __future__ import annotations

import types
from pathlib import Path

import pytest

from noteflow.infrastructure.security import keystore


def _install_fake_keyring(monkeypatch: pytest.MonkeyPatch) -> dict[tuple[str, str], str]:
    """Install a fake keyring backend backed by a dictionary."""
    storage: dict[tuple[str, str], str] = {}

    class DummyErrors:
        class KeyringError(Exception): ...

        class PasswordDeleteError(KeyringError): ...

    def get_password(service: str, key: str) -> str | None:
        return storage.get((service, key))

    def set_password(service: str, key: str, value: str) -> None:
        storage[(service, key)] = value

    def delete_password(service: str, key: str) -> None:
        storage.pop((service, key), None)

    monkeypatch.setattr(
        keystore,
        "keyring",
        types.SimpleNamespace(
            get_password=get_password,
            set_password=set_password,
            delete_password=delete_password,
            errors=DummyErrors,
        ),
    )
    return storage


def test_get_or_create_master_key_creates_and_reuses(monkeypatch: pytest.MonkeyPatch) -> None:
    """Master key should be created once and then reused."""
    storage = _install_fake_keyring(monkeypatch)
    ks = keystore.KeyringKeyStore(service_name="svc", key_name="key")

    first = ks.get_or_create_master_key()
    second = ks.get_or_create_master_key()

    assert len(first) == keystore.KEY_SIZE, "key should be KEY_SIZE bytes"
    assert first == second, "second call should return same key"
    assert ("svc", "key") in storage, "key should be stored in keyring"


def test_get_or_create_master_key_falls_back_to_file(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Keyring errors should fall back to file-based key storage."""

    class DummyErrors:
        class KeyringError(Exception): ...

    def raise_error(*_: object, **__: object) -> None:
        raise DummyErrors.KeyringError("unavailable")

    monkeypatch.setattr(
        keystore,
        "keyring",
        types.SimpleNamespace(
            get_password=raise_error,
            set_password=raise_error,
            errors=DummyErrors,
            delete_password=raise_error,
        ),
    )
    # Use temp path for file fallback
    key_file = tmp_path / ".master_key"
    monkeypatch.setattr(keystore, "DEFAULT_KEY_FILE", key_file)

    ks = keystore.KeyringKeyStore()
    key = ks.get_or_create_master_key()

    assert len(key) == keystore.KEY_SIZE, "fallback key should be KEY_SIZE bytes"
    assert key_file.exists(), "key should be persisted to file on fallback"


def test_delete_master_key_handles_missing(monkeypatch: pytest.MonkeyPatch) -> None:
    """delete_master_key should swallow missing-key errors."""
    storage = _install_fake_keyring(monkeypatch)

    class DummyErrors:
        class KeyringError(Exception): ...

        class PasswordDeleteError(KeyringError): ...

    # Reinstall with errors that raise on delete to exercise branch
    def delete_password(*_: object, **__: object) -> None:
        raise DummyErrors.PasswordDeleteError("not found")

    def get_password(service: str, key: str) -> str | None:
        return storage.get((service, key))

    def set_password(service: str, key: str, value: str) -> str | None:
        return storage.setdefault((service, key), value)

    monkeypatch.setattr(
        keystore,
        "keyring",
        types.SimpleNamespace(
            get_password=get_password,
            set_password=set_password,
            delete_password=delete_password,
            errors=DummyErrors,
        ),
    )

    ks = keystore.KeyringKeyStore()
    # Should not raise even when delete_password errors
    ks.delete_master_key()


def test_has_master_key_false_on_errors(monkeypatch: pytest.MonkeyPatch) -> None:
    """has_master_key should return False when keyring raises."""

    class DummyErrors:
        class KeyringError(Exception): ...

    def raise_error(*_: object, **__: object) -> None:
        raise DummyErrors.KeyringError("oops")

    def noop_delete(service: str, key: str) -> None:
        pass

    def noop_set(service: str, key: str, value: str) -> None:
        pass

    monkeypatch.setattr(
        keystore,
        "keyring",
        types.SimpleNamespace(
            get_password=raise_error,
            errors=DummyErrors,
            delete_password=noop_delete,
            set_password=noop_set,
        ),
    )

    ks = keystore.KeyringKeyStore()
    assert ks.has_master_key() is False, "should return False when keyring raises"


class TestFileKeyStore:
    """Tests for FileKeyStore fallback implementation."""

    def test_creates_and_reuses_key(self, tmp_path: Path) -> None:
        """File key store should create key once and reuse it."""
        key_file = tmp_path / ".master_key"
        fks = keystore.FileKeyStore(key_file)

        first = fks.get_or_create_master_key()
        second = fks.get_or_create_master_key()

        assert len(first) == keystore.KEY_SIZE, "key should be KEY_SIZE bytes"
        assert first == second, "second call should return same key"
        assert key_file.exists(), "key should be persisted to file"

    def test_creates_parent_directories(self, tmp_path: Path) -> None:
        """File key store should create parent directories."""
        key_file = tmp_path / "nested" / "dir" / ".master_key"
        fks = keystore.FileKeyStore(key_file)

        fks.get_or_create_master_key()

        assert key_file.exists(), "parent directories should be created"

    def test_has_master_key_true_when_exists(self, tmp_path: Path) -> None:
        """has_master_key should return True when file exists."""
        key_file = tmp_path / ".master_key"
        fks = keystore.FileKeyStore(key_file)
        fks.get_or_create_master_key()

        assert fks.has_master_key() is True, "should return True when key file exists"

    def test_has_master_key_false_when_missing(self, tmp_path: Path) -> None:
        """has_master_key should return False when file is missing."""
        key_file = tmp_path / ".master_key"
        fks = keystore.FileKeyStore(key_file)

        assert fks.has_master_key() is False, "should return False when key file missing"

    def test_delete_master_key_removes_file(self, tmp_path: Path) -> None:
        """delete_master_key should remove the key file."""
        key_file = tmp_path / ".master_key"
        fks = keystore.FileKeyStore(key_file)
        fks.get_or_create_master_key()

        fks.delete_master_key()

        assert not key_file.exists(), "key file should be deleted"

    def test_delete_master_key_safe_when_missing(self, tmp_path: Path) -> None:
        """delete_master_key should not raise when file is missing."""
        key_file = tmp_path / ".master_key"
        fks = keystore.FileKeyStore(key_file)

        fks.delete_master_key()  # Should not raise

    def test_invalid_base64_raises_runtime_error(self, tmp_path: Path) -> None:
        """Invalid base64 in key file should raise RuntimeError."""
        key_file = tmp_path / ".master_key"
        key_file.write_text("not-valid-base64!!!")
        fks = keystore.FileKeyStore(key_file)

        with pytest.raises(RuntimeError, match="invalid base64"):
            fks.get_or_create_master_key()

    def test_wrong_size_raises_runtime_error(self, tmp_path: Path) -> None:
        """Wrong key size in file should raise RuntimeError."""
        import base64

        key_file = tmp_path / ".master_key"
        # Write a key that's too short (16 bytes instead of 32)
        key_file.write_text(base64.b64encode(b"short_key").decode())
        fks = keystore.FileKeyStore(key_file)

        with pytest.raises(RuntimeError, match="wrong key size"):
            fks.get_or_create_master_key()
