"""Tests for eBay OAuth2 flow: code exchange, token refresh, routes, and token store."""

from __future__ import annotations

import json
import time
from unittest.mock import patch

import httpx
import pytest
from fastapi.testclient import TestClient

from agent_samochodowy.compliance.ebay_deletion import app, settings
from agent_samochodowy.compliance.ebay_oauth import (
    EbayTokenStore,
    exchange_code_for_tokens,
    get_valid_access_token,
    refresh_access_token,
)


# ------------------------------------------------------------------
# Fixtures
# ------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _override_settings(monkeypatch: pytest.MonkeyPatch) -> None:
    """Inject test settings for every test."""
    monkeypatch.setattr(settings, "ebay_client_id", "test-client-id")
    monkeypatch.setattr(settings, "ebay_client_secret", "test-client-secret")
    monkeypatch.setattr(settings, "ebay_redirect_uri", "Test_RuName")
    monkeypatch.setattr(settings, "ebay_verification_token", "tokenabc123")
    monkeypatch.setattr(
        settings,
        "ebay_deletion_endpoint_url",
        "https://api.agent.dgx.dev/ebay/deletions",
    )


@pytest.fixture
def client() -> TestClient:
    return TestClient(app)


@pytest.fixture
def token_file(tmp_path):
    """Provide a temp file path for token storage."""
    return str(tmp_path / "ebay_tokens.json")


def _mock_response(status_code: int, json_data: dict) -> httpx.Response:
    """Create an httpx.Response with a dummy request (needed for raise_for_status)."""
    request = httpx.Request("POST", "https://api.ebay.com/identity/v1/oauth2/token")
    return httpx.Response(status_code, json=json_data, request=request)


@pytest.fixture
def mock_exchange():
    """Mock the HTTP call for code→token exchange."""
    token_response = {
        "access_token": "v^1.1#new-access-token",
        "token_type": "User Access Token",
        "expires_in": 7200,
        "refresh_token": "v^1.1#new-refresh-token",
        "refresh_token_expires_in": 47304000,
    }
    mock_resp = _mock_response(200, token_response)
    with patch("agent_samochodowy.compliance.ebay_oauth.httpx.post", return_value=mock_resp) as m:
        yield m, token_response


@pytest.fixture
def mock_refresh():
    """Mock the HTTP call for refresh→token."""
    token_response = {
        "access_token": "v^1.1#refreshed-access-token",
        "token_type": "User Access Token",
        "expires_in": 7200,
    }
    mock_resp = _mock_response(200, token_response)
    with patch("agent_samochodowy.compliance.ebay_oauth.httpx.post", return_value=mock_resp) as m:
        yield m, token_response


# ------------------------------------------------------------------
# Unit: exchange_code_for_tokens
# ------------------------------------------------------------------

class TestExchangeCode:
    def test_exchange_returns_tokens(self, mock_exchange):
        mock_post, expected = mock_exchange
        result = exchange_code_for_tokens(
            code="auth-code-123",
            client_id="test-id",
            client_secret="test-secret",
            redirect_uri="Test_RuName",
        )
        assert result["access_token"] == expected["access_token"]
        assert result["refresh_token"] == expected["refresh_token"]

        # Verify correct params sent
        call_kwargs = mock_post.call_args
        assert call_kwargs.kwargs["data"]["grant_type"] == "authorization_code"
        assert call_kwargs.kwargs["data"]["code"] == "auth-code-123"
        assert "Basic " in call_kwargs.kwargs["headers"]["Authorization"]

    def test_exchange_raises_on_error(self):
        error_resp = _mock_response(401, {"error": "invalid_grant"})
        with patch("agent_samochodowy.compliance.ebay_oauth.httpx.post", return_value=error_resp):
            with pytest.raises(httpx.HTTPStatusError):
                exchange_code_for_tokens("bad-code", "id", "secret", "uri")


# ------------------------------------------------------------------
# Unit: refresh_access_token
# ------------------------------------------------------------------

class TestRefreshToken:
    def test_refresh_returns_new_access_token(self, mock_refresh):
        mock_post, expected = mock_refresh
        result = refresh_access_token(
            refresh_token="old-refresh",
            client_id="test-id",
            client_secret="test-secret",
        )
        assert result["access_token"] == expected["access_token"]

        call_kwargs = mock_post.call_args
        assert call_kwargs.kwargs["data"]["grant_type"] == "refresh_token"
        assert call_kwargs.kwargs["data"]["refresh_token"] == "old-refresh"


# ------------------------------------------------------------------
# Unit: EbayTokenStore
# ------------------------------------------------------------------

class TestTokenStore:
    def test_save_and_load(self, token_file):
        store = EbayTokenStore(token_file)
        store.save({
            "access_token": "at-123",
            "refresh_token": "rt-456",
            "expires_in": 7200,
        })
        assert store.access_token == "at-123"
        assert store.refresh_token == "rt-456"

    def test_save_merges_existing(self, token_file):
        store = EbayTokenStore(token_file)
        store.save({"access_token": "at-1", "refresh_token": "rt-1", "expires_in": 7200})
        # Refresh response doesn't include refresh_token
        store.save({"access_token": "at-2", "expires_in": 7200})
        assert store.access_token == "at-2"
        assert store.refresh_token == "rt-1"  # preserved

    def test_file_permissions(self, token_file):
        import os
        import stat
        store = EbayTokenStore(token_file)
        store.save({"access_token": "at"})
        mode = os.stat(token_file).st_mode
        assert stat.S_IMODE(mode) == 0o600

    def test_is_expired_no_file(self, token_file):
        store = EbayTokenStore(token_file)
        assert store.is_expired is True

    def test_is_expired_fresh_token(self, token_file):
        store = EbayTokenStore(token_file)
        store.save({"access_token": "at", "expires_in": 7200})
        assert store.is_expired is False

    def test_is_expired_old_token(self, token_file):
        store = EbayTokenStore(token_file)
        store.save({"access_token": "at", "expires_in": 7200})
        # Manually backdate
        import json as j
        data = j.loads(open(token_file).read())
        data["updated_at"] = time.time() - 8000
        open(token_file, "w").write(j.dumps(data))
        assert store.is_expired is True

    def test_load_missing_file(self, tmp_path):
        store = EbayTokenStore(str(tmp_path / "nonexistent.json"))
        assert store.load() == {}
        assert store.access_token is None


# ------------------------------------------------------------------
# Unit: get_valid_access_token
# ------------------------------------------------------------------

class TestGetValidAccessToken:
    def test_returns_cached_if_fresh(self, token_file):
        store = EbayTokenStore(token_file)
        store.save({"access_token": "cached-at", "refresh_token": "rt", "expires_in": 7200})
        result = get_valid_access_token("cid", "csecret", store)
        assert result == "cached-at"

    def test_refreshes_if_expired(self, token_file, mock_refresh):
        _, expected = mock_refresh
        store = EbayTokenStore(token_file)
        store.save({"access_token": "old", "refresh_token": "rt-good", "expires_in": 7200})
        # Backdate
        data = json.loads(open(token_file).read())
        data["updated_at"] = time.time() - 8000
        open(token_file, "w").write(json.dumps(data))

        result = get_valid_access_token("cid", "csecret", store)
        assert result == expected["access_token"]

    def test_raises_without_refresh_token(self, token_file):
        store = EbayTokenStore(token_file)
        # No refresh token at all
        with pytest.raises(RuntimeError, match="No refresh_token"):
            get_valid_access_token("cid", "csecret", store)


# ------------------------------------------------------------------
# Integration: OAuth callback route
# ------------------------------------------------------------------

class TestOAuthCallbackRoute:
    def test_callback_success(self, client, mock_exchange, tmp_path, monkeypatch):
        monkeypatch.setattr(settings, "ebay_token_path", str(tmp_path / "tokens.json"))
        resp = client.get("/ebay/oauth/callback", params={"code": "auth-code-xyz"})
        assert resp.status_code == 200
        assert "Autoryzacja eBay OK" in resp.text

        # Verify tokens were saved
        store = EbayTokenStore(str(tmp_path / "tokens.json"))
        assert store.access_token == "v^1.1#new-access-token"
        assert store.refresh_token == "v^1.1#new-refresh-token"

    def test_callback_missing_code_400(self, client):
        resp = client.get("/ebay/oauth/callback")
        assert resp.status_code == 400

    def test_callback_exchange_failure(self, client, tmp_path, monkeypatch):
        monkeypatch.setattr(settings, "ebay_token_path", str(tmp_path / "tokens.json"))
        error_resp = httpx.Response(401, json={"error": "invalid_grant"})
        with patch("agent_samochodowy.compliance.ebay_deletion.exchange_code_for_tokens",
                    side_effect=httpx.HTTPStatusError("401", request=None, response=error_resp)):
            resp = client.get("/ebay/oauth/callback", params={"code": "bad-code"})
        assert resp.status_code == 500
        assert "nie powiodła się" in resp.text


# ------------------------------------------------------------------
# Integration: OAuth declined + privacy routes
# ------------------------------------------------------------------

class TestOAuthDeclinedRoute:
    def test_declined_200(self, client):
        resp = client.get("/ebay/oauth/declined")
        assert resp.status_code == 200
        assert "odrzucona" in resp.text


class TestPrivacyRoute:
    def test_privacy_200(self, client):
        resp = client.get("/privacy")
        assert resp.status_code == 200
        assert "Polityka Prywatności" in resp.text
