"""Tests for the shared Google OAuth module."""

import json
import time
from unittest.mock import AsyncMock, patch

import pytest

import fliiq.runtime.google_auth as google_auth


# --- load_tokens ---


def test_load_tokens_missing(tmp_path, monkeypatch):
    """Returns empty dict when token file doesn't exist."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.setattr(google_auth, "_OLD_TOKENS_PATH", tmp_path / "old_none.json")
    assert google_auth.load_tokens() == {}


def test_load_tokens_valid(tmp_path, monkeypatch):
    """Reads tokens from file."""
    token_file = tmp_path / "tokens.json"
    data = {"test@gmail.com": {"access_token": "tok", "refresh_token": "rt", "expires_at": 9999999999}}
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)
    assert google_auth.load_tokens() == data


def test_load_tokens_malformed(tmp_path, monkeypatch):
    """Returns empty dict on malformed JSON."""
    token_file = tmp_path / "tokens.json"
    token_file.write_text("{bad json")
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)
    assert google_auth.load_tokens() == {}


def test_load_tokens_migration(tmp_path, monkeypatch):
    """Auto-migrates from old google_calendar_tokens.json path."""
    old_file = tmp_path / "old.json"
    new_file = tmp_path / "new.json"
    data = {"user@gmail.com": {"access_token": "t", "refresh_token": "r", "expires_at": 0}}
    old_file.write_text(json.dumps(data))

    monkeypatch.setattr(google_auth, "TOKENS_PATH", new_file)
    monkeypatch.setattr(google_auth, "_OLD_TOKENS_PATH", old_file)

    result = google_auth.load_tokens()
    assert result == data
    assert new_file.exists()
    assert not old_file.exists()


# --- save_tokens ---


def test_save_tokens(tmp_path, monkeypatch):
    """Writes tokens to file."""
    token_file = tmp_path / "subdir" / "tokens.json"
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)
    data = {"user@gmail.com": {"access_token": "t"}}
    google_auth.save_tokens(data)
    assert json.loads(token_file.read_text()) == data


# --- get_access_token ---


async def test_get_access_token_valid(tmp_path, monkeypatch):
    """Returns token when not expired."""
    token_file = tmp_path / "tokens.json"
    data = {
        "user@gmail.com": {
            "access_token": "valid_tok",
            "refresh_token": "rt",
            "expires_at": time.time() + 3600,
        }
    }
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)

    email, token = await google_auth.get_access_token()
    assert email == "user@gmail.com"
    assert token == "valid_tok"


async def test_get_access_token_specific_account(tmp_path, monkeypatch):
    """Returns correct account when specified."""
    token_file = tmp_path / "tokens.json"
    data = {
        "a@gmail.com": {"access_token": "tok_a", "refresh_token": "rt", "expires_at": time.time() + 3600},
        "b@gmail.com": {"access_token": "tok_b", "refresh_token": "rt", "expires_at": time.time() + 3600},
    }
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)

    email, token = await google_auth.get_access_token("b@gmail.com")
    assert email == "b@gmail.com"
    assert token == "tok_b"


async def test_get_access_token_no_tokens(tmp_path, monkeypatch):
    """Raises ValueError when no tokens file."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    with pytest.raises(ValueError, match="fliiq google auth"):
        await google_auth.get_access_token()


async def test_get_access_token_account_not_found(tmp_path, monkeypatch):
    """Raises ValueError when requested account not in tokens."""
    token_file = tmp_path / "tokens.json"
    token_file.write_text(json.dumps({"a@gmail.com": {"access_token": "t", "refresh_token": "r", "expires_at": 9999999999}}))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)

    with pytest.raises(ValueError, match="No tokens for"):
        await google_auth.get_access_token("other@gmail.com")


async def test_get_access_token_refresh(tmp_path, monkeypatch):
    """Refreshes expired token and updates file."""
    import httpx

    token_file = tmp_path / "tokens.json"
    data = {
        "user@gmail.com": {
            "access_token": "old",
            "refresh_token": "rt",
            "expires_at": 0,
        }
    }
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)
    monkeypatch.setenv("GOOGLE_CLIENT_ID", "cid")
    monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "csec")

    def mock_transport(request):
        return httpx.Response(200, json={"access_token": "new_tok", "expires_in": 3600})

    real_client = httpx.AsyncClient

    def patched_client(**kw):
        kw["transport"] = httpx.MockTransport(mock_transport)
        return real_client(**kw)

    with patch("fliiq.runtime.google_auth.httpx.AsyncClient", side_effect=patched_client):
        email, token = await google_auth.get_access_token()

    assert token == "new_tok"
    updated = json.loads(token_file.read_text())
    assert updated["user@gmail.com"]["access_token"] == "new_tok"


# --- resolve_gmail_creds ---


async def test_resolve_gmail_creds_oauth(tmp_path, monkeypatch):
    """Resolves to OAuth when token exists."""
    token_file = tmp_path / "tokens.json"
    data = {
        "user@gmail.com": {
            "access_token": "oauth_tok",
            "refresh_token": "rt",
            "expires_at": time.time() + 3600,
        }
    }
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)

    addr, method, cred = await google_auth.resolve_gmail_creds()
    assert addr == "user@gmail.com"
    assert method == "oauth2"
    assert cred == "oauth_tok"


async def test_resolve_gmail_creds_oauth_specific_account(tmp_path, monkeypatch):
    """Resolves to OAuth for specific account."""
    token_file = tmp_path / "tokens.json"
    data = {
        "a@gmail.com": {"access_token": "tok_a", "refresh_token": "rt", "expires_at": time.time() + 3600},
        "b@gmail.com": {"access_token": "tok_b", "refresh_token": "rt", "expires_at": time.time() + 3600},
    }
    token_file.write_text(json.dumps(data))
    monkeypatch.setattr(google_auth, "TOKENS_PATH", token_file)

    addr, method, cred = await google_auth.resolve_gmail_creds("b@gmail.com")
    assert addr == "b@gmail.com"
    assert method == "oauth2"
    assert cred == "tok_b"


async def test_resolve_gmail_creds_app_password_fallback(tmp_path, monkeypatch):
    """Falls back to app password when no OAuth token (new env var names)."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.setenv("FLIIQ_GMAIL_ADDRESS", "bot@gmail.com")
    monkeypatch.setenv("FLIIQ_GMAIL_APP_PASSWORD", "apppass")

    addr, method, cred = await google_auth.resolve_gmail_creds()
    assert addr == "bot@gmail.com"
    assert method == "app_password"
    assert cred == "apppass"


async def test_resolve_gmail_creds_old_env_vars_fallback(tmp_path, monkeypatch):
    """Falls back to old GMAIL_ADDRESS/GMAIL_APP_PASSWORD env var names."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.delenv("FLIIQ_GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("FLIIQ_GMAIL_APP_PASSWORD", raising=False)
    monkeypatch.setenv("GMAIL_ADDRESS", "old@gmail.com")
    monkeypatch.setenv("GMAIL_APP_PASSWORD", "oldpass")

    addr, method, cred = await google_auth.resolve_gmail_creds()
    assert addr == "old@gmail.com"
    assert method == "app_password"
    assert cred == "oldpass"


async def test_resolve_gmail_creds_param_app_password(tmp_path, monkeypatch):
    """Falls back to app password from params."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.delenv("GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("GMAIL_APP_PASSWORD", raising=False)

    addr, method, cred = await google_auth.resolve_gmail_creds("work@gmail.com", "work-pass")
    assert addr == "work@gmail.com"
    assert method == "app_password"
    assert cred == "work-pass"


async def test_resolve_gmail_creds_no_credentials(tmp_path, monkeypatch):
    """Raises ValueError when no OAuth and no app password."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.delenv("FLIIQ_GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("FLIIQ_GMAIL_APP_PASSWORD", raising=False)
    monkeypatch.delenv("GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("GMAIL_APP_PASSWORD", raising=False)

    with pytest.raises(ValueError, match="No Gmail credentials"):
        await google_auth.resolve_gmail_creds()


async def test_resolve_gmail_creds_address_no_password(tmp_path, monkeypatch):
    """Raises ValueError when address set but no password and no OAuth."""
    monkeypatch.setattr(google_auth, "TOKENS_PATH", tmp_path / "none.json")
    monkeypatch.setenv("FLIIQ_GMAIL_ADDRESS", "test@gmail.com")
    monkeypatch.delenv("FLIIQ_GMAIL_APP_PASSWORD", raising=False)
    monkeypatch.delenv("GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("GMAIL_APP_PASSWORD", raising=False)

    with pytest.raises(ValueError, match="FLIIQ_GMAIL_APP_PASSWORD"):
        await google_auth.resolve_gmail_creds()


# --- get_fliiq_email ---


def test_get_fliiq_email_new_var(monkeypatch):
    """Returns FLIIQ_GMAIL_ADDRESS when set."""
    monkeypatch.setenv("FLIIQ_GMAIL_ADDRESS", "bot@fliiq.com")
    assert google_auth.get_fliiq_email() == "bot@fliiq.com"


def test_get_fliiq_email_old_var_fallback(monkeypatch):
    """Falls back to GMAIL_ADDRESS when FLIIQ_GMAIL_ADDRESS not set."""
    monkeypatch.delenv("FLIIQ_GMAIL_ADDRESS", raising=False)
    monkeypatch.setenv("GMAIL_ADDRESS", "old@gmail.com")
    assert google_auth.get_fliiq_email() == "old@gmail.com"


def test_get_fliiq_email_none(monkeypatch):
    """Returns None when no env var set."""
    monkeypatch.delenv("FLIIQ_GMAIL_ADDRESS", raising=False)
    monkeypatch.delenv("GMAIL_ADDRESS", raising=False)
    assert google_auth.get_fliiq_email() is None
