"""Tests for encrypted credential storage."""

from __future__ import annotations

from pathlib import Path

import pytest
from cryptography.exceptions import InvalidTag

from better_telegram_mcp.transports.credential_store import CredentialStore


@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
    d = tmp_path / "data"
    d.mkdir()
    return d


class TestCredentialStore:
    def test_store_load_roundtrip(self, data_dir: Path) -> None:
        """Credentials can be stored and loaded back correctly."""
        store = CredentialStore(data_dir, secret="test-secret")
        creds = {
            "TELEGRAM_BOT_TOKEN": "123456:ABC-DEF",
            "TELEGRAM_API_ID": "12345",
        }
        store.store(creds)
        loaded = store.load()
        assert loaded == creds

    def test_load_returns_none_when_no_file(self, data_dir: Path) -> None:
        """Loading from empty store returns None."""
        store = CredentialStore(data_dir, secret="test-secret")
        assert store.load() is None

    def test_different_secrets_produce_different_encryption(
        self, data_dir: Path
    ) -> None:
        """Different secrets should not decrypt each other's data."""
        store1 = CredentialStore(data_dir, secret="secret-one")
        creds = {"TELEGRAM_BOT_TOKEN": "token123"}
        store1.store(creds)

        # Read raw encrypted bytes
        enc_path = data_dir / "credentials.enc"
        encrypted_data = enc_path.read_bytes()

        # Try to decrypt with different secret -- should fail
        store2 = CredentialStore(data_dir, secret="secret-two")
        with pytest.raises(InvalidTag):
            store2.load()

        # Original secret still works
        store1_again = CredentialStore(data_dir, secret="secret-one")
        assert store1_again.load() == creds

        # Verify the encrypted file is still the same (not corrupted by failed load)
        assert enc_path.read_bytes() == encrypted_data

    def test_delete_removes_file(self, data_dir: Path) -> None:
        """Delete should remove the credentials file."""
        store = CredentialStore(data_dir, secret="test-secret")
        creds = {"TELEGRAM_BOT_TOKEN": "token123"}
        store.store(creds)

        enc_path = data_dir / "credentials.enc"
        assert enc_path.exists()

        store.delete()
        assert not enc_path.exists()

    def test_delete_noop_when_no_file(self, data_dir: Path) -> None:
        """Delete should not raise when no file exists."""
        store = CredentialStore(data_dir, secret="test-secret")
        store.delete()  # Should not raise

    def test_auto_generated_secret_persists(self, data_dir: Path) -> None:
        """Auto-generated secret should be saved and reused across instances."""
        store1 = CredentialStore(data_dir)
        creds = {"TELEGRAM_BOT_TOKEN": "token123"}
        store1.store(creds)

        # New instance should auto-load the persisted secret
        store2 = CredentialStore(data_dir)
        assert store2.load() == creds

    def test_auto_generated_secret_file_created(self, data_dir: Path) -> None:
        """Secret file should be created when no secret is provided."""
        CredentialStore(data_dir)
        secret_path = data_dir / ".secret"
        assert secret_path.exists()
        secret = secret_path.read_text().strip()
        assert len(secret) == 64  # 32 bytes hex-encoded

    def test_env_var_secret_takes_precedence(
        self, data_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """CREDENTIAL_SECRET env var should be used when set."""
        monkeypatch.setenv("CREDENTIAL_SECRET", "env-secret")
        store = CredentialStore(data_dir)
        creds = {"TELEGRAM_BOT_TOKEN": "token123"}
        store.store(creds)

        # Should load with same env var
        store2 = CredentialStore(data_dir)
        assert store2.load() == creds

        # Should not load without env var (different auto-generated secret)
        monkeypatch.delenv("CREDENTIAL_SECRET")
        store3 = CredentialStore(data_dir)
        # Auto-generated secret is different from "env-secret"
        with pytest.raises(InvalidTag):
            store3.load()

    def test_store_overwrites_existing(self, data_dir: Path) -> None:
        """Storing new credentials should overwrite old ones."""
        store = CredentialStore(data_dir, secret="test-secret")
        store.store({"TELEGRAM_BOT_TOKEN": "old-token"})
        store.store({"TELEGRAM_BOT_TOKEN": "new-token"})
        assert store.load() == {"TELEGRAM_BOT_TOKEN": "new-token"}

    def test_empty_credentials(self, data_dir: Path) -> None:
        """Empty dict should be storable and loadable."""
        store = CredentialStore(data_dir, secret="test-secret")
        store.store({})
        assert store.load() == {}

    def test_data_dir_created_if_missing(self, tmp_path: Path) -> None:
        """Store should create data_dir if it does not exist."""
        nested = tmp_path / "a" / "b" / "c"
        store = CredentialStore(nested, secret="test-secret")
        store.store({"key": "value"})
        assert store.load() == {"key": "value"}

    def test_legacy_salt_migration(self, data_dir: Path) -> None:
        """Credentials stored with legacy hardcoded salt should be loadable,
        and re-storing should migrate to a random salt."""
        from better_telegram_mcp.transports.credential_store import _LEGACY_SALT

        store = CredentialStore(data_dir, secret="test-secret")
        # Simulate legacy: write credentials with legacy salt
        # (new install creates random salt, so we need to force legacy)
        store._salt = _LEGACY_SALT
        store._cached_key = None
        # Write a credentials file (using legacy salt)
        creds = {"TELEGRAM_BOT_TOKEN": "legacy-token"}
        # Manually encrypt and write without triggering migration
        import json
        import os

        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        key = store._derive_key()
        aesgcm = AESGCM(key)
        nonce = os.urandom(12)
        plaintext = json.dumps(creds).encode()
        ciphertext = aesgcm.encrypt(nonce, plaintext, None)
        store._path.write_bytes(nonce + ciphertext)

        # Remove salt file to simulate legacy state
        salt_path = data_dir / ".salt"
        if salt_path.exists():
            salt_path.unlink()

        # Create new store -- should detect legacy salt (creds exist, no .salt)
        store2 = CredentialStore(data_dir, secret="test-secret")
        assert store2._salt == _LEGACY_SALT
        loaded = store2.load()
        assert loaded == creds

        # Re-store should trigger salt migration
        store2.store(creds)
        assert store2._salt != _LEGACY_SALT
        assert salt_path.exists()

        # New store should use the migrated salt
        store3 = CredentialStore(data_dir, secret="test-secret")
        assert store3._salt != _LEGACY_SALT
        assert store3.load() == creds

    def test_random_salt_for_new_install(self, data_dir: Path) -> None:
        """New installation should generate random salt, not use legacy."""
        from better_telegram_mcp.transports.credential_store import _LEGACY_SALT

        store = CredentialStore(data_dir, secret="test-secret")
        assert store._salt != _LEGACY_SALT
        salt_path = data_dir / ".salt"
        assert salt_path.exists()
        assert len(store._salt) == 16

    def test_chmod_failure_swallowed(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ) -> None:
        """Test that OSError during chmod is silently ignored."""

        def mock_chmod(*args, **kwargs):
            raise OSError("chmod failed")

        monkeypatch.setattr("pathlib.Path.chmod", mock_chmod)

        store = CredentialStore(tmp_path)
        # Store writing triggers credential chmod
        store.store({"api_id": "123"})
