"""Test connection-level auth: encrypted storage, auth resolution, OAuth refresh."""

import json
from datetime import datetime, timedelta
from unittest.mock import patch, MagicMock

import pytest
from click.testing import CliRunner

from mcpbundles.config import AuthError, get_or_create_key
from mcpbundles.connections import ConnectionManager, ConnectionInfo, _encrypt, _decrypt


@pytest.fixture
def runner():
    return CliRunner()


@pytest.fixture
def clean_sessions(tmp_path, monkeypatch):
    """Redirect SESSIONS_DIR to a temp folder for isolation."""
    test_sessions = tmp_path / "sessions"
    test_sessions.mkdir()
    monkeypatch.setattr("mcpbundles.connections.SESSIONS_DIR", test_sessions)
    monkeypatch.delenv("MCPBUNDLES_API_KEY", raising=False)
    return test_sessions


# ── Encryption primitives ──────────────────────────────────────────


class TestEncryption:
    def test_roundtrip(self):
        plaintext = "mb_test_key_12345"
        encrypted = _encrypt(plaintext)
        assert encrypted != plaintext
        assert _decrypt(encrypted) == plaintext

    def test_different_plaintexts_produce_different_ciphertexts(self):
        a = _encrypt("key_one")
        b = _encrypt("key_two")
        assert a != b

    def test_decrypt_invalid_token_returns_none_via_connection(self, clean_sessions):
        conn = ConnectionInfo(
            name="bad",
            url="https://api.mcpbundles.com",
            endpoint="/hub/",
            workspace_id=None,
            session_id=None,
            is_default=False,
            created_at=datetime.utcnow().isoformat(),
            auth_token_encrypted="not-a-valid-fernet-token",
        )
        assert conn.get_auth_token() is None


# ── ConnectionInfo auth properties ─────────────────────────────────


class TestConnectionInfoAuth:
    def test_has_auth_false_by_default(self):
        conn = ConnectionInfo(
            name="test", url="https://x.com", endpoint="/hub/",
            workspace_id=None, session_id=None, is_default=False,
            created_at=datetime.utcnow().isoformat(),
        )
        assert not conn.has_auth
        assert conn.auth_type == "none"
        assert conn.get_auth_token() is None

    def test_set_and_get_api_key(self):
        conn = ConnectionInfo(
            name="test", url="https://x.com", endpoint="/hub/",
            workspace_id=None, session_id=None, is_default=False,
            created_at=datetime.utcnow().isoformat(),
        )
        conn.set_auth_token("mb_my_api_key")
        assert conn.has_auth
        assert conn.auth_type == "api-key"
        assert conn.get_auth_token() == "mb_my_api_key"

    def test_set_and_get_oauth_data(self):
        conn = ConnectionInfo(
            name="test", url="https://x.com", endpoint="/hub/",
            workspace_id=None, session_id=None, is_default=False,
            created_at=datetime.utcnow().isoformat(),
        )
        conn.set_auth_token("access_token_xyz")
        conn.set_refresh_data({
            "client_id": "cid",
            "client_secret": "csec",
            "refresh_token": "rt_abc",
            "expires_at": "2099-01-01T00:00:00",
        })
        assert conn.auth_type == "oauth"
        assert conn.get_auth_token() == "access_token_xyz"
        refresh = conn.get_refresh_data()
        assert refresh["client_id"] == "cid"
        assert refresh["refresh_token"] == "rt_abc"

    def test_clear_auth(self):
        conn = ConnectionInfo(
            name="test", url="https://x.com", endpoint="/hub/",
            workspace_id=None, session_id=None, is_default=False,
            created_at=datetime.utcnow().isoformat(),
        )
        conn.set_auth_token("mb_key")
        conn.clear_auth()
        assert not conn.has_auth
        assert conn.auth_type == "none"


# ── ConnectionManager encrypted persistence ────────────────────────


class TestConnectionManagerAuth:
    def test_create_with_api_key(self, clean_sessions):
        mgr = ConnectionManager()
        conn = mgr.create(
            name="prod",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="mb_production_key",
        )
        assert conn.get_auth_token() == "mb_production_key"
        assert conn.auth_type == "api-key"

        reloaded = mgr.get("prod")
        assert reloaded is not None
        assert reloaded.get_auth_token() == "mb_production_key"

    def test_create_with_oauth(self, clean_sessions):
        mgr = ConnectionManager()
        refresh = {
            "client_id": "cid",
            "client_secret": "csec",
            "refresh_token": "rt_xyz",
            "expires_at": "2099-01-01T00:00:00",
        }
        conn = mgr.create(
            name="oauth_conn",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="access_123",
            refresh_data=refresh,
        )
        assert conn.auth_type == "oauth"

        reloaded = mgr.get("oauth_conn")
        assert reloaded.get_auth_token() == "access_123"
        assert reloaded.get_refresh_data()["refresh_token"] == "rt_xyz"

    def test_encrypted_fields_not_plaintext_on_disk(self, clean_sessions):
        mgr = ConnectionManager()
        mgr.create(
            name="secret",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="mb_super_secret_key",
        )
        disk_data = json.loads((clean_sessions / "secret.json").read_text())
        assert disk_data["auth_token_encrypted"] is not None
        assert "mb_super_secret_key" not in disk_data["auth_token_encrypted"]

    def test_update_auth(self, clean_sessions):
        mgr = ConnectionManager()
        mgr.create(name="upd", url="https://x.com", endpoint="/hub/", auth_token="old_key")
        assert mgr.get("upd").get_auth_token() == "old_key"

        mgr.update_auth("upd", "new_key")
        assert mgr.get("upd").get_auth_token() == "new_key"

    def test_update_auth_nonexistent_returns_false(self, clean_sessions):
        mgr = ConnectionManager()
        assert mgr.update_auth("ghost", "key") is False

    def test_file_permissions(self, clean_sessions):
        mgr = ConnectionManager()
        mgr.create(name="perm", url="https://x.com", endpoint="/hub/", auth_token="mb_k")
        path = clean_sessions / "perm.json"
        mode = oct(path.stat().st_mode)[-3:]
        assert mode == "600"

    def test_list_connections_shows_auth_type(self, clean_sessions):
        mgr = ConnectionManager()
        mgr.create(name="a", url="https://x.com", endpoint="/hub/")
        mgr.create(name="b", url="https://x.com", endpoint="/hub/", auth_token="mb_k")
        mgr.create(
            name="c", url="https://x.com", endpoint="/hub/",
            auth_token="tok", refresh_data={"client_id": "x", "client_secret": "y", "refresh_token": "z", "expires_at": "2099-01-01"},
        )
        conns = {c.name: c.auth_type for c in mgr.list_connections()}
        assert conns["a"] == "none"
        assert conns["b"] == "api-key"
        assert conns["c"] == "oauth"

    def test_backward_compat_no_auth_fields(self, clean_sessions):
        """Connections created before auth persistence still load fine."""
        legacy_data = {
            "name": "legacy",
            "url": "https://x.com",
            "endpoint": "/hub/",
            "workspace_id": None,
            "session_id": None,
            "is_default": False,
            "created_at": "2026-01-01T00:00:00",
        }
        (clean_sessions / "legacy.json").write_text(json.dumps(legacy_data))
        mgr = ConnectionManager()
        conn = mgr.get("legacy")
        assert conn is not None
        assert conn.auth_type == "none"
        assert conn.get_auth_token() is None


# ── Auth resolution from _resolve_connection ───────────────────────


class TestAuthHierarchy:
    def test_stored_auth_wins_over_env_var(self, clean_sessions, monkeypatch):
        """Connection stored auth is highest priority — most intentional."""
        mgr = ConnectionManager()
        mgr.create(name="test", url="https://x.com", endpoint="/hub/", auth_token="stored_key", is_default=True)

        monkeypatch.setenv("MCPBUNDLES_API_KEY", "env_key")

        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        result = resolve_auth(CLISettings.resolve(), "test")
        assert result.token == "stored_key"

    def test_env_var_used_when_no_stored_auth(self, clean_sessions, monkeypatch):
        """Env var is second in hierarchy when connection has no stored auth."""
        mgr = ConnectionManager()
        mgr.create(name="bare", url="https://x.com", endpoint="/hub/", is_default=True)

        monkeypatch.setenv("MCPBUNDLES_API_KEY", "env_key")

        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        result = resolve_auth(CLISettings.resolve(), "bare")
        assert result.token == "env_key"

    def test_stored_key_used_when_no_env(self, clean_sessions):
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        mgr = ConnectionManager()
        mgr.create(name="prod", url="https://mcp.mcpbundles.com", endpoint="/hub/", auth_token="mb_stored")

        result = resolve_auth(CLISettings.resolve(), "prod")
        assert result.token == "mb_stored"

    def test_default_connection_auth(self, clean_sessions):
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        mgr = ConnectionManager()
        mgr.create(name="default_one", url="https://x.com", endpoint="/hub/", auth_token="mb_default", is_default=True)

        result = resolve_auth(CLISettings.resolve(), None)
        assert result.token == "mb_default"
        assert result.conn_name == "default_one"

    def test_single_connection_auto_selected(self, clean_sessions):
        """One connection = no --as needed, progressive disclosure."""
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        mgr = ConnectionManager()
        mgr.create(name="only", url="https://x.com", endpoint="/hub/", auth_token="mb_only")

        result = resolve_auth(CLISettings.resolve(), None)
        assert result.token == "mb_only"
        assert result.conn_name == "only"

    def test_multiple_connections_no_default_errors(self, clean_sessions):
        """Multiple connections without a default requires explicit --as."""
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        mgr = ConnectionManager()
        mgr.create(name="a", url="https://x.com", endpoint="/hub/", auth_token="mb_a")
        mgr.create(name="b", url="https://x.com", endpoint="/hub/", auth_token="mb_b")

        with pytest.raises(SystemExit):
            resolve_auth(CLISettings.resolve(), None)

    def test_zero_connections_uses_env_var(self, clean_sessions, monkeypatch):
        """No connections — env var works for ad-hoc usage."""
        monkeypatch.setenv("MCPBUNDLES_API_KEY", "mb_adhoc")

        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        result = resolve_auth(CLISettings.resolve(), None)
        assert result.token == "mb_adhoc"
        assert result.conn_name is None

    def test_no_auth_anywhere_raises_with_actionable_message(self, clean_sessions):
        """When nothing is configured, error tells the user all three options."""
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        with patch("mcpbundles.config.load_token", return_value=None):
            with pytest.raises(AuthError) as exc_info:
                resolve_auth(CLISettings.resolve(), None)

        msg = str(exc_info.value)
        assert "mcpbundles connect" in msg
        assert "MCPBUNDLES_API_KEY" in msg
        assert "mcpbundles login" in msg

    def test_connection_not_found_raises(self, clean_sessions):
        from mcpbundles.auth_resolve import resolve_auth
        from mcpbundles.config import CLISettings

        with pytest.raises(SystemExit):
            resolve_auth(CLISettings.resolve(), "ghost")


# ── OAuth auto-refresh ─────────────────────────────────────────────


class TestOAuthRefresh:
    def test_fresh_token_not_refreshed(self, clean_sessions):
        mgr = ConnectionManager()
        future = (datetime.now() + timedelta(hours=1)).isoformat()
        mgr.create(
            name="fresh",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="access_fresh",
            refresh_data={
                "client_id": "cid",
                "client_secret": "csec",
                "refresh_token": "rt",
                "expires_at": future,
            },
        )
        from mcpbundles.auth_resolve import _get_stored_auth
        conn = mgr.get("fresh")
        result = _get_stored_auth(conn, mgr)
        assert result == "access_fresh"

    def test_expired_token_triggers_refresh(self, clean_sessions):
        mgr = ConnectionManager()
        past = (datetime.now() - timedelta(hours=1)).isoformat()
        mgr.create(
            name="expired",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="old_access",
            refresh_data={
                "client_id": "cid",
                "client_secret": "csec",
                "refresh_token": "rt_old",
                "expires_at": past,
            },
        )

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "new_access",
            "refresh_token": "rt_new",
            "expires_in": 3600,
        }

        with patch("mcpbundles.auth_resolve.requests.post", return_value=mock_response) as mock_post:
            from mcpbundles.auth_resolve import _get_stored_auth
            conn = mgr.get("expired")
            result = _get_stored_auth(conn, mgr)

        assert result == "new_access"
        mock_post.assert_called_once()

        refreshed = mgr.get("expired")
        assert refreshed.get_auth_token() == "new_access"
        assert refreshed.get_refresh_data()["refresh_token"] == "rt_new"

    def test_failed_refresh_returns_stale_token(self, clean_sessions):
        mgr = ConnectionManager()
        past = (datetime.now() - timedelta(hours=1)).isoformat()
        mgr.create(
            name="stale",
            url="https://mcp.mcpbundles.com",
            endpoint="/hub/",
            auth_token="stale_access",
            refresh_data={
                "client_id": "cid",
                "client_secret": "csec",
                "refresh_token": "rt",
                "expires_at": past,
            },
        )

        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.text = "invalid_grant"

        with patch("mcpbundles.auth_resolve.requests.post", return_value=mock_response):
            from mcpbundles.auth_resolve import _get_stored_auth
            conn = mgr.get("stale")
            result = _get_stored_auth(conn, mgr)

        assert result == "stale_access"
