/**
 * apiContracts.test.ts — Testes de integração: contratos frontend ↔ backend
 *
 * Verificam que os serviços do frontend enviam os campos corretos para o backend
 * e que o mapeamento de respostas está correto.
 *
 * Esses testes usam mocks do apiClient para simular respostas do backend e
 * verificar que o frontend interpreta corretamente o formato retornado.
 *
 * Execute com:
 *   pnpm test src/tests/integration/apiContracts.test.ts
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { authService } from '../../services/auth'
import { shippingService } from '../../services/shippingService'
import { ordersService } from '../../services/ordersService'
import { apiClient } from '../../services/apiClient'

vi.mock('../../services/apiClient', () => ({
    apiClient: {
        get: vi.fn(),
        post: vi.fn(),
        put: vi.fn(),
        patch: vi.fn(),
        delete: vi.fn(),
        postMultiPart: vi.fn(),
        setToken: vi.fn(),
    }
}))

const mockedApiClient = vi.mocked(apiClient)

beforeEach(() => {
    vi.clearAllMocks()
})

// =============================================================================
// Contrato Auth
// =============================================================================

describe('Contrato Auth', () => {
    describe('login — campos enviados e recebidos', () => {
        it('envia {username, password} e mapeia {access_token, expires_at} para {token, expiresAt}', async () => {
            // Simula resposta REAL do backend Go
            const backendResponse = {
                access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test',
                expires_at: '2026-02-25T20:00:00Z',
            }
            mockedApiClient.post.mockResolvedValueOnce(backendResponse)

            const result = await authService.login({ username: 'joao', password: 'S3nh@!' })

            // Verifica que o frontend enviou os campos corretos
            expect(mockedApiClient.post).toHaveBeenCalledWith('/v1/auth/login', {
                username: 'joao',
                password: 'S3nh@!',
            })

            // Verifica que o frontend mapeou a resposta corretamente
            expect(result.token).toBe(backendResponse.access_token)
            expect(result.expiresAt).toBe(backendResponse.expires_at)
        })

        it('propaga erro 401 sem mascarar', async () => {
            const error = Object.assign(new Error('Invalid credentials'), {
                response: { status: 401, data: { error: 'invalid credentials' } },
            })
            mockedApiClient.post.mockRejectedValueOnce(error)

            await expect(authService.login({ username: 'x', password: 'y' })).rejects.toThrow()
        })

        it('logout chama POST /v1/auth/logout', async () => {
            mockedApiClient.post.mockResolvedValueOnce(undefined)
            await authService.logout()
            expect(mockedApiClient.post).toHaveBeenCalledWith('/v1/auth/logout')
        })
    })
})

// =============================================================================
// Contrato Shipping — DIVERGÊNCIAS DOCUMENTADAS
// =============================================================================

describe('Contrato Shipping', () => {
    /**
     * DIVERGÊNCIA 1: O payload enviado pelo shippingService.calculate não
     * corresponde ao que o backend espera.
     *
     * Frontend envia: {buyer_id, order_total_cents, items[]}
     * Backend espera: {vendor_id, cart_total_cents, buyer_latitude, buyer_longitude}
     *
     * Este teste documenta o payload ATUAL (com divergência).
     * Quando a divergência for corrigida, altere o payload para o formato correto.
     */
    describe('[DIVERGÊNCIA 1] calculate — payload atual vs esperado pelo backend', () => {
        it('ATUAL: frontend envia buyer_id + order_total_cents + items[] (backend rejeita)', async () => {
            // Simula o 400 que o backend retorna
            const error = Object.assign(new Error('vendor_id is required'), {
                response: { status: 400, data: { error: 'vendor_id is required' } },
            })
            mockedApiClient.post.mockRejectedValueOnce(error)

            await expect(shippingService.calculate({
                buyer_id: 'buyer-123',
                order_total_cents: 15000,
                items: [{ seller_id: 'seller-1', product_id: 'prod-1', quantity: 2, price_cents: 7500 }],
            })).rejects.toThrow()

            // Confirma que o frontend enviou os campos ERRADOS para o backend
            expect(mockedApiClient.post).toHaveBeenCalledWith(
                '/v1/shipping/calculate',
                expect.objectContaining({
                    buyer_id: 'buyer-123',        // ← NÃO existe no contrato do backend
                    order_total_cents: 15000,      // ← backend chama: cart_total_cents
                })
            )

            // O frontend NÃO envia o que o backend precisa:
            expect(mockedApiClient.post).not.toHaveBeenCalledWith(
                '/v1/shipping/calculate',
                expect.objectContaining({
                    vendor_id: expect.any(String),    // ← obrigatório no backend
                    buyer_latitude: expect.any(Number), // ← obrigatório no backend
                })
            )
        })

        it('CORRETO (pós-correção): deve enviar vendor_id + cart_total_cents + buyer_lat/lng', async () => {
            // Este teste mostra como DEVERIA ser após a correção da Divergência 1.
            // O formato atual do shippingService.calculate não suporta esses campos ainda.
            const backendShippingOptions = [
                { type: 'delivery', description: 'Entrega padrão', value_cents: 2500, estimated_minutes: 120 },
                { type: 'pickup', description: 'Rua Teste, 123', value_cents: 0, estimated_minutes: 0 },
            ]
            mockedApiClient.post.mockResolvedValueOnce(backendShippingOptions)

            // WORKAROUND atual: fazer a chamada diretamente ao apiClient
            // (pois o shippingService.calculate não aceita esses campos)
            const result = await apiClient.post('/v1/shipping/calculate', {
                vendor_id: 'seller-uuid-here',
                cart_total_cents: 15000,
                buyer_latitude: -23.56,
                buyer_longitude: -46.64,
            })

            expect(result).toEqual(backendShippingOptions)
        })
    })

    /**
     * DIVERGÊNCIA 2: Os campos da resposta do backend não batem com o que
     * o frontend espera.
     *
     * Backend retorna: {type, description, value_cents, estimated_minutes}
     * Frontend espera: {seller_id, delivery_fee_cents, distance_km, estimated_days, pickup_available}
     */
    describe('[DIVERGÊNCIA 2] calculate — response do backend vs esperado pelo frontend', () => {
        it('documenta mapeamento incorreto dos campos de resposta', async () => {
            // Resposta REAL do backend
            const backendResponse = [
                {
                    type: 'delivery',
                    description: 'Entrega padrão',
                    value_cents: 2500,        // ← frontend espera: delivery_fee_cents
                    estimated_minutes: 120,   // ← frontend espera: estimated_days
                },
            ]
            mockedApiClient.post.mockResolvedValueOnce({ options: backendResponse })

            const result = await shippingService.calculate({
                buyer_id: 'buyer-123',
                order_total_cents: 5000,
                items: [],
            })

            // O frontend acessa campos que NÃO existem na resposta real do backend
            if (result.options && result.options.length > 0) {
                const opt = result.options[0]

                // Esses campos são undefined — o frontend vai mostrar NaN/undefined para o usuário
                expect(opt.delivery_fee_cents).toBeUndefined() // ← campo não existe; backend usa value_cents
                expect(opt.estimated_days).toBeUndefined()     // ← campo não existe; backend usa estimated_minutes
                expect(opt.distance_km).toBeUndefined()        // ← campo não existe na resposta do backend
                expect(opt.pickup_available).toBeUndefined()   // ← campo não existe; backend usa type="pickup"
            }
        })
    })

    describe('getSettings — contrato alinhado', () => {
        it('GET /v1/shipping/settings/{vendorId} retorna ShippingSettings', async () => {
            const mockSettings = {
                vendor_id: 'vendor-123',
                active: true,
                max_radius_km: 50,
                price_per_km_cents: 150,
                min_fee_cents: 1000,
                pickup_active: true,
                pickup_address: 'Rua Teste, 123',
                pickup_hours: '08:00-18:00',
                latitude: -23.55,
                longitude: -46.63,
            }
            mockedApiClient.get.mockResolvedValueOnce(mockSettings)

            const result = await shippingService.getSettings('vendor-123')

            expect(mockedApiClient.get).toHaveBeenCalledWith('/v1/shipping/settings/vendor-123')
            expect(result.vendor_id).toBe('vendor-123')
            expect(result.active).toBe(true)
        })
    })
})

// =============================================================================
// Contrato Orders — DIVERGÊNCIA 3 documentada
// =============================================================================

describe('Contrato Orders', () => {
    /**
     * DIVERGÊNCIA 3: O campo payment_method é enviado como string pelo frontend,
     * mas o backend espera um objeto {type: string, installments: number}.
     *
     * Frontend envia: "payment_method": "pix"
     * Backend espera: "payment_method": {"type": "pix", "installments": 1}
     */
    describe('[DIVERGÊNCIA 3] createOrder — payment_method string vs objeto', () => {
        it('ATUAL: frontend envia payment_method como string (backend lê type vazio)', async () => {
            const mockOrder = {
                id: 'order-new',
                status: 'Pendente',
                payment_method: '',  // backend retorna vazio porque não conseguiu mapear a string
                total_cents: 3000,
            }
            mockedApiClient.post.mockResolvedValueOnce(mockOrder)

            await ordersService.createOrder({
                buyer_id: 'buyer-1',
                seller_id: 'seller-1',
                items: [{ product_id: 'prod-1', quantity: 1, unit_cents: 3000, batch: 'B01', expires_at: '2025-12-31' }],
                shipping: {
                    recipient_name: 'Teste',
                    street: 'Rua',
                    number: '1',
                    district: 'Bairro',
                    city: 'Cidade',
                    state: 'SP',
                    zip_code: '00000-000',
                    country: 'Brasil',
                },
                payment_method: 'pix', // ← string (formato atual do frontend)
            })

            // Confirma que o frontend está enviando string, não objeto
            expect(mockedApiClient.post).toHaveBeenCalledWith(
                '/v1/orders',
                expect.objectContaining({
                    payment_method: 'pix', // ← string, não {type: 'pix', installments: 1}
                })
            )
        })

        it('CORRETO (pós-correção): payment_method deve ser {type, installments}', async () => {
            // Simula chamada correta após correção da divergência
            // O CreateOrderRequest precisaria aceitar o objeto no lugar da string.
            mockedApiClient.post.mockResolvedValueOnce({ id: 'order-ok', status: 'Pendente' })

            // Chamada direta ao apiClient para simular o formato correto
            await apiClient.post('/v1/orders', {
                seller_id: 'seller-1',
                items: [],
                shipping: {},
                payment_method: { type: 'pix', installments: 1 }, // ← formato correto
            })

            expect(mockedApiClient.post).toHaveBeenCalledWith(
                '/v1/orders',
                expect.objectContaining({
                    payment_method: { type: 'pix', installments: 1 },
                })
            )
        })
    })

    describe('listOrders — contrato alinhado', () => {
        it('GET /v1/orders retorna lista paginada', async () => {
            const mockResponse = {
                orders: [
                    { id: 'o1', status: 'Pendente', total_cents: 10000 },
                ],
                total: 1,
                page: 1,
                page_size: 20,
            }
            mockedApiClient.get.mockResolvedValueOnce(mockResponse)

            const result = await ordersService.listOrders()

            expect(mockedApiClient.get).toHaveBeenCalledWith('/v1/orders')
            expect(result).toEqual(mockResponse)
        })

        it('propaga erros de rede', async () => {
            mockedApiClient.get.mockRejectedValueOnce(new Error('Network Error'))
            await expect(ordersService.listOrders()).rejects.toThrow('Network Error')
        })
    })

    describe('reorder', () => {
        it('POST /v1/orders/{id}/reorder retorna {cart, warnings}', async () => {
            const mockReorder = { cart: [{ product_id: 'prod-1', quantity: 2 }], warnings: [] }
            mockedApiClient.post.mockResolvedValueOnce(mockReorder)

            const result = await ordersService.reorder('order-abc')

            expect(mockedApiClient.post).toHaveBeenCalledWith('/v1/orders/order-abc/reorder')
            expect(result).toEqual(mockReorder)
        })
    })
})

// =============================================================================
// Contrato API Client — Comportamentos do interceptor
// =============================================================================

describe('Contrato apiClient', () => {
    it('[DIVERGÊNCIA 4] interceptor 401: deve limpar token e redirecionar (atualmente apenas loga)', () => {
        /**
         * DIVERGÊNCIA 4: O interceptor de resposta do apiClient apenas loga
         * um aviso em caso de 401, mas não limpa o token nem redireciona.
         *
         * Comportamento atual: logger.warn('Sessão expirada...')
         * Comportamento esperado: apiClient.setToken(null) + localStorage.clear + redirect /login
         *
         * Este teste documenta a limitação. Para testar o comportamento correto,
         * seria necessário injetar o interceptor e verificar chamadas a localStorage
         * e window.location.
         */
        // Verificação documental: o teste não pode rodar automaticamente sem
        // acessar o interceptor do axios, mas serve como documentação da divergência.
        expect(true).toBe(true) // placeholder — ver docs/API_DIVERGENCES.md Divergência 4
    })
})
