"""
Tests for the Amazon Consume API client

These tests verify:
1. The POST carries the Bearer token and payload to the configured URL
2. Outcomes map per the spec: 200/ACKNOWLEDGED, 200/ERRORED and 400 are
   terminal; 403 and repeated 401 are auth failures; 408/429/5xx/network
   retry with backoff until exhausted
3. A first 401 refreshes the token exactly once and retries immediately
4. Every request/response emits the structured JSON trace line
"""

import asyncio
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import patch

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

import app.src.amazon_consume_client as client_mod
from app.src.amazon_auth import AmazonAuthError
from app.src.amazon_consume_client import AmazonConsumeClient, DeliveryOutcome

URL = "https://gamma.example.com/v1/consultation/response"


class MockConfig:
    amazon_consultation_response_url = URL
    amazon_api_timeout_seconds = 1.0
    amazon_api_retry_attempts = 3
    amazon_api_retry_delay_seconds = 0.001


class FakeTokenProvider:
    def __init__(self, tokens=("tok-1",), error=None):
        self.tokens = list(tokens)
        self.error = error
        self.invalidations = 0

    async def get_token(self):
        if self.error is not None:
            raise self.error
        return self.tokens[0] if len(self.tokens) == 1 else self.tokens.pop(0)

    def invalidate(self):
        self.invalidations += 1


class FakeResponse:
    def __init__(self, status, json_body=None, text_body=""):
        self.status = status
        self._json = json_body
        self._text = text_body

    async def json(self, content_type=None):
        return self._json

    async def text(self):
        return self._text

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        return False


class FailingPost:
    def __init__(self, exc):
        self.exc = exc

    async def __aenter__(self):
        raise self.exc

    async def __aexit__(self, *args):
        return False


class FakeSession:
    def __init__(self, outcomes):
        self.outcomes = list(outcomes)
        self.calls = []

    def post(self, url, **kwargs):
        self.calls.append((url, kwargs))
        outcome = self.outcomes.pop(0)
        if isinstance(outcome, Exception):
            return FailingPost(outcome)
        return outcome

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        return False


def payload():
    return {
        "messageIdentifiers": {
            "primaryReferenceId": "404-1234567-1234567",
            "secondaryReferenceId": "cons-123",
            "trackingId": "trk-abc",
            "providerName": "myrx",
            "isTestData": False,
        },
        "data": {
            "responseMetadata": {"messageSentTime": 1, "messageType": "CONSULTATION_COMPLETED"},
            "consultationResponse": {"status": "SUCCESS", "customerCallAttemptNumber": 1},
        },
    }


def acknowledged_response():
    return FakeResponse(
        200,
        json_body={"status": "ACKNOWLEDGED", "acknowledgementTimeStamp": 1699968883231},
    )


class ClientTestCase(unittest.TestCase):
    def send(self, outcomes, token_provider=None):
        self.token_provider = token_provider or FakeTokenProvider()
        client = AmazonConsumeClient(MockConfig(), token_provider=self.token_provider)
        session = FakeSession(outcomes)
        with patch.object(client_mod.aiohttp, "ClientSession", return_value=session):
            result = asyncio.run(client.send_consultation_response(payload()))
        return result, session


class TestRequestShape(ClientTestCase):
    def test_posts_payload_with_bearer_token(self):
        result, session = self.send([acknowledged_response()])

        self.assertIs(result.outcome, DeliveryOutcome.ACKNOWLEDGED)
        self.assertEqual(result.acknowledgement_timestamp, 1699968883231)
        self.assertEqual(result.http_status, 200)
        self.assertEqual(result.attempts, 1)
        self.assertTrue(result.acknowledged)

        url, kwargs = session.calls[0]
        self.assertEqual(url, URL)
        self.assertEqual(kwargs["headers"]["Authorization"], "Bearer tok-1")
        self.assertEqual(kwargs["headers"]["Content-Type"], "application/json")
        self.assertEqual(kwargs["json"], payload())


class TestTerminalOutcomes(ClientTestCase):
    def test_body_errored_is_terminal_with_message(self):
        result, session = self.send(
            [
                FakeResponse(
                    200,
                    json_body={
                        "status": "ERRORED",
                        "errorMessage": "Missing required field: consultationResponse.status",
                    },
                )
            ]
        )

        self.assertIs(result.outcome, DeliveryOutcome.ERRORED)
        self.assertIn("Missing required field", result.error_message)
        self.assertEqual(len(session.calls), 1)

    def test_400_is_terminal(self):
        result, session = self.send([FakeResponse(400, text_body="malformed body")])

        self.assertIs(result.outcome, DeliveryOutcome.ERRORED)
        self.assertEqual(result.http_status, 400)
        self.assertIn("malformed body", result.error_message)
        self.assertEqual(len(session.calls), 1)

    def test_403_is_auth_failure(self):
        result, session = self.send([FakeResponse(403, text_body="wrong scope")])

        self.assertIs(result.outcome, DeliveryOutcome.AUTH_FAILED)
        self.assertEqual(len(session.calls), 1)


class TestAuthRefresh(ClientTestCase):
    def test_first_401_refreshes_once_and_retries(self):
        provider = FakeTokenProvider(tokens=["tok-old", "tok-new"])
        result, session = self.send(
            [FakeResponse(401, text_body="expired"), acknowledged_response()],
            token_provider=provider,
        )

        self.assertIs(result.outcome, DeliveryOutcome.ACKNOWLEDGED)
        self.assertEqual(provider.invalidations, 1)
        self.assertEqual(len(session.calls), 2)
        self.assertEqual(
            session.calls[1][1]["headers"]["Authorization"], "Bearer tok-new"
        )

    def test_second_401_is_hard_auth_failure(self):
        result, session = self.send(
            [FakeResponse(401, text_body="expired"), FakeResponse(401, text_body="expired")]
        )

        self.assertIs(result.outcome, DeliveryOutcome.AUTH_FAILED)
        self.assertEqual(result.http_status, 401)
        self.assertEqual(len(session.calls), 2)

    def test_non_retryable_token_error_fails_without_calling_api(self):
        provider = FakeTokenProvider(
            error=AmazonAuthError("invalid_client", status=401, retryable=False)
        )
        result, session = self.send([], token_provider=provider)

        self.assertIs(result.outcome, DeliveryOutcome.AUTH_FAILED)
        self.assertEqual(session.calls, [])


class TestTransientRetries(ClientTestCase):
    def test_retries_5xx_then_succeeds(self):
        result, session = self.send(
            [FakeResponse(500, text_body="boom"), acknowledged_response()]
        )

        self.assertIs(result.outcome, DeliveryOutcome.ACKNOWLEDGED)
        self.assertEqual(len(session.calls), 2)

    def test_retries_429_and_408(self):
        result, session = self.send(
            [
                FakeResponse(429, text_body="throttled"),
                FakeResponse(408, text_body="timeout"),
                acknowledged_response(),
            ]
        )

        self.assertIs(result.outcome, DeliveryOutcome.ACKNOWLEDGED)
        self.assertEqual(len(session.calls), 3)

    def test_retries_network_error(self):
        result, session = self.send(
            [
                client_mod.aiohttp.ClientConnectionError("refused"),
                acknowledged_response(),
            ]
        )

        self.assertIs(result.outcome, DeliveryOutcome.ACKNOWLEDGED)
        self.assertEqual(len(session.calls), 2)

    def test_exhausts_after_three_attempts(self):
        result, session = self.send(
            [
                FakeResponse(500, text_body="a"),
                FakeResponse(503, text_body="b"),
                asyncio.TimeoutError(),
            ]
        )

        self.assertIs(result.outcome, DeliveryOutcome.TRANSIENT_EXHAUSTED)
        self.assertEqual(result.attempts, 3)
        self.assertEqual(len(session.calls), 3)


class TestStructuredLog(ClientTestCase):
    def test_emits_json_trace_lines_for_request_and_response(self):
        with self.assertLogs("app.src.amazon_consume_client", level="INFO") as logs:
            self.send([acknowledged_response()])

        events = [
            json.loads(line.split(":", 2)[2])
            for line in logs.output
            if line.split(":", 2)[2].startswith("{")
        ]
        by_event = {e["event"]: e for e in events}
        self.assertIn("consume_api_request", by_event)
        self.assertIn("consume_api_response", by_event)

        response_event = by_event["consume_api_response"]
        self.assertEqual(response_event["primaryReferenceId"], "404-1234567-1234567")
        self.assertEqual(response_event["trackingId"], "trk-abc")
        self.assertEqual(response_event["providerName"], "myrx")
        self.assertEqual(response_event["customerCallAttemptNumber"], 1)
        self.assertEqual(response_event["httpStatus"], 200)
        self.assertEqual(response_event["status"], "ACKNOWLEDGED")
        self.assertEqual(response_event["acknowledgementTimeStamp"], 1699968883231)
        self.assertIn("timestampIst", response_event)


if __name__ == "__main__":
    unittest.main()
