# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

import io
import json
import urllib.error
from unittest.mock import patch

import pytest

from oauth_draft.credentials import (
    Credentials,
    locate_credentials,
    refresh_access_token,
)


def write_creds(path, **overrides):
    data = {
        "client_id": "id-123",
        "client_secret": "secret-456",
        "refresh_token": "refresh-789",
        "from_address": "you@example.com",
    }
    data.update(overrides)
    path.write_text(json.dumps(data))
    return path


def test_load_full_record(tmp_path):
    p = write_creds(tmp_path / "c.json")
    creds = Credentials.load(p)
    assert creds.client_id == "id-123"
    assert creds.client_secret == "secret-456"
    assert creds.refresh_token == "refresh-789"
    assert creds.from_address == "you@example.com"


def test_load_missing_required_field(tmp_path):
    p = tmp_path / "c.json"
    p.write_text(json.dumps({"client_id": "x", "client_secret": "y"}))
    with pytest.raises(SystemExit) as excinfo:
        Credentials.load(p)
    assert "refresh_token" in str(excinfo.value)
    assert "from_address" in str(excinfo.value)


def test_load_without_from_address_when_not_required(tmp_path):
    p = tmp_path / "c.json"
    p.write_text(json.dumps({"client_id": "x", "client_secret": "y", "refresh_token": "z"}))
    creds = Credentials.load(p, require_from_address=False)
    assert creds.from_address is None


def test_load_empty_value_treated_as_missing(tmp_path):
    p = write_creds(tmp_path / "c.json", refresh_token="")
    with pytest.raises(SystemExit) as excinfo:
        Credentials.load(p)
    assert "refresh_token" in str(excinfo.value)


def test_locate_explicit_wins(tmp_path, monkeypatch):
    explicit = tmp_path / "explicit.json"
    explicit.write_text("{}")
    other = tmp_path / "other.json"
    other.write_text("{}")
    monkeypatch.setenv("GMAIL_OAUTH_CREDENTIALS", str(other))
    assert locate_credentials(str(explicit)) == explicit


def test_locate_falls_back_to_env(tmp_path, monkeypatch):
    env_path = tmp_path / "env.json"
    env_path.write_text("{}")
    monkeypatch.setenv("GMAIL_OAUTH_CREDENTIALS", str(env_path))
    assert locate_credentials(None) == env_path


def test_locate_raises_when_nothing_exists(tmp_path, monkeypatch):
    monkeypatch.delenv("GMAIL_OAUTH_CREDENTIALS", raising=False)
    monkeypatch.setattr(
        "oauth_draft.credentials.DEFAULT_CREDENTIALS_PATH",
        tmp_path / "does-not-exist.json",
    )
    with pytest.raises(SystemExit) as excinfo:
        locate_credentials(None)
    assert "No Gmail OAuth credentials found" in str(excinfo.value)


# --- refresh_access_token --------------------------------------------------


CREDS = Credentials(
    client_id="cid",
    client_secret="secret",
    refresh_token="refresh",
    from_address="me@example.com",
)


class _FakeResponse:
    """Minimal context-manager stand-in for urllib.request.urlopen()."""

    def __init__(self, payload: bytes):
        self._payload = payload

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return False

    def read(self):
        return self._payload


def test_refresh_access_token_returns_access_token():
    with patch("oauth_draft.credentials.urllib.request.urlopen") as mock_open:
        mock_open.return_value = _FakeResponse(b'{"access_token": "abc-123"}')
        token = refresh_access_token(CREDS)
    assert token == "abc-123"
    # The POST body must include all four OAuth refresh-flow params.
    request = mock_open.call_args.args[0]
    assert request.method == "POST"
    assert request.full_url == "https://oauth2.googleapis.com/token"
    body = request.data.decode()
    assert "client_id=cid" in body
    assert "client_secret=secret" in body
    assert "refresh_token=refresh" in body
    assert "grant_type=refresh_token" in body


def test_refresh_access_token_raises_on_http_error():
    err = urllib.error.HTTPError(
        url="https://oauth2.googleapis.com/token",
        code=400,
        msg="Bad Request",
        hdrs=None,  # type: ignore[arg-type]
        fp=io.BytesIO(b'{"error": "invalid_grant"}'),
    )
    with patch("oauth_draft.credentials.urllib.request.urlopen", side_effect=err):
        with pytest.raises(SystemExit) as excinfo:
            refresh_access_token(CREDS)
    assert "OAuth token refresh failed (400)" in str(excinfo.value)
    assert "invalid_grant" in str(excinfo.value)


def test_refresh_access_token_raises_when_no_token_in_payload():
    with patch("oauth_draft.credentials.urllib.request.urlopen") as mock_open:
        mock_open.return_value = _FakeResponse(b'{"expires_in": 3600}')
        with pytest.raises(SystemExit) as excinfo:
            refresh_access_token(CREDS)
    assert "no access_token" in str(excinfo.value)
