"""
Tests for the Wave Business API SDK.

Run with:
    pip install pytest pytest-mock responses
    pytest tests/ -v
"""

import hashlib
import hmac
import json
import time
import unittest
from unittest.mock import MagicMock, patch

import responses
import pytest

from wave_business_api import WaveClient, WebhookVerifier
from wave_business_api.exceptions import (
    WaveAPIError,
    WaveAuthError,
    WaveNotFoundError,
    WaveRateLimitError,
    WaveValidationError,
    WaveWebhookError,
)

API_KEY = "wave_sn_prod_test_key_1234"
BASE_URL = "https://api.wave.com"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_client(**kwargs):
    return WaveClient(api_key=API_KEY, **kwargs)


def mock_get(url, json_body, status=200):
    responses.add(responses.GET, url, json=json_body, status=status)


def mock_post(url, json_body=None, status=200):
    responses.add(responses.POST, url, json=json_body or {}, status=status)


# ---------------------------------------------------------------------------
# WaveClient
# ---------------------------------------------------------------------------

class TestWaveClient(unittest.TestCase):

    def test_repr_masks_api_key(self):
        client = make_client()
        assert "1234" in repr(client)
        assert API_KEY not in repr(client)

    def test_has_all_resources(self):
        client = make_client()
        assert hasattr(client, "balance")
        assert hasattr(client, "checkout")
        assert hasattr(client, "payout")
        assert hasattr(client, "aggregated_merchants")


# ---------------------------------------------------------------------------
# Balance API
# ---------------------------------------------------------------------------

class TestBalanceAPI(unittest.TestCase):

    @responses.activate
    def test_get_balance(self):
        mock_get(f"{BASE_URL}/v1/balance", {"amount": "10245", "currency": "XOF"})
        client = make_client()
        balance = client.balance.get()
        assert balance["amount"] == "10245"
        assert balance["currency"] == "XOF"

    @responses.activate
    def test_get_balance_with_subaccounts(self):
        mock_get(f"{BASE_URL}/v1/balance", {"amount": "20000", "currency": "XOF"})
        client = make_client()
        balance = client.balance.get(include_subaccounts=True)
        assert "include_subaccounts" in responses.calls[0].request.url

    @responses.activate
    def test_get_transactions(self):
        payload = {
            "page_info": {"has_next_page": False, "end_cursor": None},
            "date": "2024-01-15",
            "items": [{"transaction_id": "T_ABC", "amount": "100"}],
        }
        mock_get(f"{BASE_URL}/v1/transactions", payload)
        client = make_client()
        result = client.balance.get_transactions(date="2024-01-15")
        assert len(result["items"]) == 1

    @responses.activate
    def test_get_all_transactions_pagination(self):
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/transactions",
            json={
                "page_info": {"has_next_page": True, "end_cursor": "cursor1"},
                "items": [{"transaction_id": "T_1", "amount": "100"}],
            },
            status=200,
        )
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/transactions",
            json={
                "page_info": {"has_next_page": False, "end_cursor": "cursor2"},
                "items": [{"transaction_id": "T_2", "amount": "200"}],
            },
            status=200,
        )
        client = make_client()
        all_tx = list(client.balance.get_all_transactions())
        assert len(all_tx) == 2

    @responses.activate
    def test_refund_transaction(self):
        responses.add(responses.POST, f"{BASE_URL}/v1/transactions/T_ABC/refund", status=200, body=b"")
        client = make_client()
        result = client.balance.refund_transaction("T_ABC")
        assert result is None


# ---------------------------------------------------------------------------
# Checkout API
# ---------------------------------------------------------------------------

class TestCheckoutAPI(unittest.TestCase):

    @responses.activate
    def test_create_session(self):
        payload = {
            "id": "cos-18qq25rgr100a",
            "amount": "1000",
            "checkout_status": "open",
            "wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a",
        }
        mock_post(f"{BASE_URL}/v1/checkout/sessions", payload)
        client = make_client()
        session = client.checkout.create_session(
            amount="1000",
            currency="XOF",
            success_url="https://example.com/success",
            error_url="https://example.com/error",
        )
        assert session["id"] == "cos-18qq25rgr100a"
        assert "wave_launch_url" in session

    @responses.activate
    def test_get_session(self):
        payload = {"id": "cos-18qq25rgr100a", "checkout_status": "complete"}
        mock_get(f"{BASE_URL}/v1/checkout/sessions/cos-18qq25rgr100a", payload)
        client = make_client()
        session = client.checkout.get_session("cos-18qq25rgr100a")
        assert session["checkout_status"] == "complete"

    @responses.activate
    def test_search_sessions(self):
        payload = {"result": [{"id": "cos-18qq25rgr100a", "client_reference": "order-42"}]}
        mock_get(f"{BASE_URL}/v1/checkout/sessions/search", payload)
        client = make_client()
        results = client.checkout.search_sessions("order-42")
        assert len(results) == 1
        assert results[0]["client_reference"] == "order-42"

    @responses.activate
    def test_expire_session(self):
        responses.add(responses.POST, f"{BASE_URL}/v1/checkout/sessions/cos-abc/expire", status=200, body=b"")
        client = make_client()
        result = client.checkout.expire_session("cos-abc")
        assert result is None

    @responses.activate
    def test_refund_session(self):
        responses.add(responses.POST, f"{BASE_URL}/v1/checkout/sessions/cos-abc/refund", status=200, body=b"")
        client = make_client()
        result = client.checkout.refund_session("cos-abc")
        assert result is None


# ---------------------------------------------------------------------------
# Payout API
# ---------------------------------------------------------------------------

class TestPayoutAPI(unittest.TestCase):

    @responses.activate
    def test_create_payout(self):
        payload = {
            "id": "pt-185b5e4b8100c",
            "status": "succeeded",
            "receive_amount": "5000",
            "fee": "50",
        }
        mock_post(f"{BASE_URL}/v1/payout", payload)
        client = make_client()
        payout = client.payout.create(
            currency="XOF",
            receive_amount="5000",
            mobile="+221555110219",
        )
        assert payout["status"] == "succeeded"
        assert payout["id"] == "pt-185b5e4b8100c"

    @responses.activate
    def test_get_payout(self):
        payload = {"id": "pt-185b5e4b8100c", "status": "succeeded"}
        mock_get(f"{BASE_URL}/v1/payout/pt-185b5e4b8100c", payload)
        client = make_client()
        payout = client.payout.get("pt-185b5e4b8100c")
        assert payout["id"] == "pt-185b5e4b8100c"

    @responses.activate
    def test_search_payouts(self):
        payload = {"result": [{"id": "pt-abc", "client_reference": "inv-001"}]}
        mock_get(f"{BASE_URL}/v1/payouts/search", payload)
        client = make_client()
        results = client.payout.search("inv-001")
        assert len(results) == 1

    @responses.activate
    def test_create_batch(self):
        mock_post(f"{BASE_URL}/v1/payout-batch", {"id": "pb-185skxq8g1006"})
        client = make_client()
        batch = client.payout.create_batch([
            {"currency": "XOF", "receive_amount": "1000", "mobile": "+221555110219"},
        ])
        assert batch["id"] == "pb-185skxq8g1006"

    @responses.activate
    def test_get_batch(self):
        payload = {"id": "pb-185skxq8g1006", "status": "complete", "payouts": []}
        mock_get(f"{BASE_URL}/v1/payout-batch/pb-185skxq8g1006", payload)
        client = make_client()
        batch = client.payout.get_batch("pb-185skxq8g1006")
        assert batch["status"] == "complete"

    @responses.activate
    def test_reverse_payout(self):
        responses.add(responses.POST, f"{BASE_URL}/v1/payout/pt-abc/reverse", status=200, body=b"")
        client = make_client()
        result = client.payout.reverse("pt-abc")
        assert result is None

    @responses.activate
    def test_verify_recipient(self):
        payload = {"within_limits": True, "name_match": "MATCH"}
        mock_post(f"{BASE_URL}/v1/verify_recipient/", payload)
        client = make_client()
        result = client.payout.verify_recipient(
            mobile="+221555110219",
            name="Fatou Ndiaye",
            amount="5000",
            currency="XOF",
        )
        assert result["name_match"] == "MATCH"
        assert result["within_limits"] is True


# ---------------------------------------------------------------------------
# Aggregated Merchants API
# ---------------------------------------------------------------------------

class TestAggregatedMerchantsAPI(unittest.TestCase):

    @responses.activate
    def test_list_merchants(self):
        payload = {
            "page_info": {"has_next_page": False, "end_cursor": "abc"},
            "items": [{"id": "am-123", "name": "Test Merchant"}],
        }
        mock_get(f"{BASE_URL}/v1/aggregated_merchants", payload)
        client = make_client()
        result = client.aggregated_merchants.list()
        assert len(result["items"]) == 1

    @responses.activate
    def test_create_merchant(self):
        payload = {
            "id": "am-new123",
            "name": "New Merchant",
            "business_type": "other",
            "is_locked": False,
        }
        mock_post(f"{BASE_URL}/v1/aggregated_merchants", payload)
        client = make_client()
        merchant = client.aggregated_merchants.create(
            name="New Merchant",
            business_type="other",
            business_description="Test business",
        )
        assert merchant["id"] == "am-new123"

    @responses.activate
    def test_get_merchant(self):
        payload = {"id": "am-7lks22ap113t4", "name": "Test", "is_locked": True}
        mock_get(f"{BASE_URL}/v1/aggregated_merchants/am-7lks22ap113t4", payload)
        client = make_client()
        merchant = client.aggregated_merchants.get("am-7lks22ap113t4")
        assert merchant["is_locked"] is True

    @responses.activate
    def test_delete_merchant(self):
        responses.add(responses.DELETE, f"{BASE_URL}/v1/aggregated_merchants/am-abc", status=204, body=b"")
        client = make_client()
        result = client.aggregated_merchants.delete("am-abc")
        assert result is None


# ---------------------------------------------------------------------------
# Error Handling
# ---------------------------------------------------------------------------

class TestErrorHandling(unittest.TestCase):

    @responses.activate
    def test_401_raises_auth_error(self):
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/balance",
            json={"code": "invalid-auth", "message": "Invalid API key"},
            status=401,
        )
        client = make_client()
        with self.assertRaises(WaveAuthError):
            client.balance.get()

    @responses.activate
    def test_404_raises_not_found_error(self):
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/payout/pt-fake",
            json={"code": "not-found", "message": "Not found"},
            status=404,
        )
        client = make_client()
        with self.assertRaises(WaveNotFoundError):
            client.payout.get("pt-fake")

    @responses.activate
    def test_429_raises_rate_limit_error(self):
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/balance",
            json={"code": "too-many-requests", "message": "Rate limit exceeded"},
            status=429,
        )
        client = make_client()
        with self.assertRaises(WaveRateLimitError):
            client.balance.get()

    @responses.activate
    def test_500_raises_server_error(self):
        from wave_business_api.exceptions import WaveServerError
        responses.add(
            responses.GET,
            f"{BASE_URL}/v1/balance",
            json={"code": "internal-server-error", "message": "Server error"},
            status=500,
        )
        client = make_client()
        with self.assertRaises(WaveServerError):
            client.balance.get()


# ---------------------------------------------------------------------------
# Webhook Verifier
# ---------------------------------------------------------------------------

class TestWebhookVerifier(unittest.TestCase):

    SIGNING_SECRET = "wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg"
    VALID_BODY = '{"id": "AE_ijzo7oGgrlM7", "type": "checkout.session.completed", "data": {"id": "cos-1b01sghpg100j", "amount": "100", "checkout_status": "complete", "client_reference": null, "currency": "XOF", "error_url": "https://example.com/error", "last_payment_error": null, "business_name": "Annas Apiaries", "payment_status": "succeeded", "success_url": "https://example.com/success", "wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries", "when_completed": "2022-11-08T15:05:45Z", "when_created": "2022-11-08T15:05:32Z", "when_expires": "2022-11-09T15:05:32Z", "transaction_id": "TCN4Y4ZC3FM"}}'
    VALID_SIGNATURE = "t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b"

    def test_verify_known_good_signature(self):
        """Test with the official Wave documentation example."""
        verifier = WebhookVerifier(
            signing_secret=self.SIGNING_SECRET,
            tolerance_seconds=0,  # 0 = disable timestamp check for static test
        )
        event = verifier.verify_and_parse(
            raw_body=self.VALID_BODY,
            wave_signature=self.VALID_SIGNATURE,
        )
        assert event["type"] == "checkout.session.completed"
        assert event["data"]["transaction_id"] == "TCN4Y4ZC3FM"

    def test_invalid_signature_raises(self):
        verifier = WebhookVerifier(signing_secret=self.SIGNING_SECRET)
        timestamp = str(int(time.time()))
        bad_sig = f"t={timestamp},v1=badhash"
        with self.assertRaises(WaveWebhookError):
            verifier.verify_and_parse(raw_body=self.VALID_BODY, wave_signature=bad_sig)

    def test_expired_timestamp_raises(self):
        verifier = WebhookVerifier(signing_secret=self.SIGNING_SECRET, tolerance_seconds=1)
        # Use a timestamp from 10 seconds ago
        old_ts = str(int(time.time()) - 10)
        payload = old_ts + self.VALID_BODY
        sig = hmac.new(
            self.SIGNING_SECRET.encode(),
            payload.encode(),
            hashlib.sha256,
        ).hexdigest()
        wave_sig = f"t={old_ts},v1={sig}"
        with self.assertRaises(WaveWebhookError):
            verifier.verify_and_parse(raw_body=self.VALID_BODY, wave_signature=wave_sig)

    def test_valid_fresh_signature(self):
        verifier = WebhookVerifier(signing_secret=self.SIGNING_SECRET)
        timestamp = str(int(time.time()))
        body = '{"id": "EV_test", "type": "test.test_event", "data": {}}'
        payload = timestamp + body
        sig = hmac.new(
            self.SIGNING_SECRET.encode(),
            payload.encode(),
            hashlib.sha256,
        ).hexdigest()
        wave_sig = f"t={timestamp},v1={sig}"
        event = verifier.verify_and_parse(raw_body=body, wave_signature=wave_sig)
        assert event["type"] == "test.test_event"

    def test_shared_secret_valid(self):
        verifier = WebhookVerifier(shared_secret="mysecret")
        body = '{"id": "EV_1", "type": "checkout.session.completed", "data": {}}'
        event = verifier.verify_and_parse(raw_body=body, authorization="Bearer mysecret")
        assert event["type"] == "checkout.session.completed"

    def test_shared_secret_invalid(self):
        verifier = WebhookVerifier(shared_secret="mysecret")
        body = '{"id": "EV_1", "type": "checkout.session.completed", "data": {}}'
        with self.assertRaises(WaveWebhookError):
            verifier.verify_and_parse(raw_body=body, authorization="Bearer wrongsecret")

    def test_requires_at_least_one_secret(self):
        with self.assertRaises(ValueError):
            WebhookVerifier()

    def test_get_event_type_helper(self):
        event = {"type": "checkout.session.completed", "data": {}}
        assert WebhookVerifier.get_event_type(event) == "checkout.session.completed"

    def test_get_event_data_helper(self):
        event = {"type": "checkout.session.completed", "data": {"amount": "100"}}
        assert WebhookVerifier.get_event_data(event)["amount"] == "100"


if __name__ == "__main__":
    unittest.main()
