import { describe, expect, it, vi } from 'vitest'
import {
  confirmDoubleOptIn,
  createMemoryComplianceStore,
  type CampaignLegalContext,
} from './compliance.js'
import { MarketingProviderError } from './index.js'
import { createBrevoDoubleOptInContact, makeBrevoAdapter } from './brevo.js'

const legal: CampaignLegalContext = {
  region: 'EU',
  oneClickUnsubscribeUrl: 'https://example.com/unsub',
  legal: {
    spf: true,
    dkim: true,
    dmarc: true,
    physicalPostalAddress: '1 Market St',
    advertisingLabel: 'Ad',
  },
}

function createFetchMock(handlers: Record<string, (init?: RequestInit) => Response | Promise<Response>>) {
  return vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
    const handler = Object.entries(handlers).find(([pattern]) => url.includes(pattern))?.[1]
    if (!handler) {
      return new Response(JSON.stringify({ message: `unmocked ${url}` }), { status: 500 })
    }
    return handler(init)
  }) as typeof fetch
}

describe('makeBrevoAdapter', () => {
  it('maps createBrevoDoubleOptInContact to Brevo native double opt-in', async () => {
    const fetchImpl = createFetchMock({
      '/contacts/doubleOptinConfirmation': () => new Response(null, { status: 204 }),
    })

    await createBrevoDoubleOptInContact(
      { apiKey: 'key', fetchImpl },
      {
        email: 'a@example.com',
        attributes: { LOCALE: 'en', SOURCE_PAGE: '/en' },
      },
      {
        listId: 12,
        templateId: 34,
        redirectionUrl: 'https://example.com/en/waitlist/confirmed',
      },
    )

    expect(fetchImpl).toHaveBeenCalledWith(
      'https://api.brevo.com/v3/contacts/doubleOptinConfirmation',
      expect.objectContaining({
        method: 'POST',
        body: JSON.stringify({
          email: 'a@example.com',
          attributes: { LOCALE: 'en', SOURCE_PAGE: '/en' },
          includeListIds: [12],
          templateId: 34,
          redirectionUrl: 'https://example.com/en/waitlist/confirmed',
        }),
      }),
    )
  })

  it('classifies native double-opt-in errors', async () => {
    const fetchImpl = createFetchMock({
      '/contacts/doubleOptinConfirmation': () =>
        new Response(JSON.stringify({ message: 'invalid template' }), { status: 400 }),
    })

    await expect(
      createBrevoDoubleOptInContact(
        { apiKey: 'key', fetchImpl },
        { email: 'a@example.com' },
        {
          listId: 12,
          templateId: 34,
          redirectionUrl: 'https://example.com/en/waitlist/confirmed',
        },
      ),
    ).rejects.toBeInstanceOf(MarketingProviderError)
  })

  it('maps upsertContact to the Brevo contacts endpoint', async () => {
    const fetchImpl = createFetchMock({
      '/contacts': () => new Response(JSON.stringify({ id: 1 }), { status: 201 }),
    })

    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    await adapter.upsertContact({ email: 'a@example.com' }, '12')

    expect(fetchImpl).toHaveBeenCalledWith(
      'https://api.brevo.com/v3/contacts',
      expect.objectContaining({
        method: 'POST',
        body: JSON.stringify({
          email: 'a@example.com',
          updateEnabled: true,
          listIds: [12],
        }),
      }),
    )
  })

  it('disables contact updates when requested', async () => {
    const fetchImpl = createFetchMock({
      '/contacts': () => new Response(JSON.stringify({ id: 1 }), { status: 201 }),
    })

    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    await adapter.upsertContact({ email: 'a@example.com' }, '12', { updateEnabled: false })

    expect(fetchImpl).toHaveBeenCalledWith(
      'https://api.brevo.com/v3/contacts',
      expect.objectContaining({
        method: 'POST',
        body: JSON.stringify({
          email: 'a@example.com',
          updateEnabled: false,
          listIds: [12],
        }),
      }),
    )
  })

  it('classifies an error response to MarketingProviderError', async () => {
    const fetchImpl = createFetchMock({
      '/contacts/lists': () =>
        new Response(JSON.stringify({ message: 'invalid api key' }), { status: 401 }),
    })

    const adapter = makeBrevoAdapter({
      apiKey: 'bad',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    await expect(adapter.createList('Weekly')).rejects.toBeInstanceOf(MarketingProviderError)
  })

  it('enforces the compliance core before the Brevo sendCampaign API call', async () => {
    // Production recipient-resolution path: GET campaign -> GET each list's contacts -> POST sendNow.
    const fetchImpl = createFetchMock({
      '/emailCampaigns/42/sendNow': () => new Response(null, { status: 204 }),
      '/contacts/lists/7/contacts': () =>
        new Response(
          JSON.stringify({ contacts: [{ email: 'ok@example.com' }, { email: 'blocked@example.com' }] }),
          { status: 200 },
        ),
      '/emailCampaigns/42': () =>
        new Response(JSON.stringify({ recipients: { lists: [7] } }), { status: 200 }),
    })

    const complianceStore = createMemoryComplianceStore({
      suppressed: [
        {
          email: 'blocked@example.com',
          reason: 'unsubscribe',
          suppressedAt: '2026-01-01T00:00:00.000Z',
        },
      ],
    })
    await confirmDoubleOptIn(complianceStore, 'ok@example.com', 'EU')

    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore,
    })

    const result = await adapter.sendCampaign('42', {
      trackingBaseUrl: 'https://track.example',
      legal,
    })

    expect(result.sent).toEqual(['ok@example.com'])
    expect(result.refused).toEqual([
      expect.objectContaining({ email: 'blocked@example.com', code: 'suppressed' }),
    ])
    expect(fetchImpl).toHaveBeenCalledWith(
      'https://api.brevo.com/v3/emailCampaigns/42/sendNow',
      expect.objectContaining({ method: 'POST' }),
    )
  })

  it('skips the Brevo sendNow call when every recipient fails the compliance gate', async () => {
    const fetchImpl = createFetchMock({
      '/emailCampaigns/99/sendNow': () => new Response(null, { status: 204 }),
      '/contacts/lists/3/contacts': () =>
        new Response(JSON.stringify({ contacts: [{ email: 'new@example.com' }] }), { status: 200 }),
      '/emailCampaigns/99': () =>
        new Response(JSON.stringify({ recipients: { lists: [3] } }), { status: 200 }),
    })

    const complianceStore = createMemoryComplianceStore()
    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore,
    })

    const result = await adapter.sendCampaign('99', {
      trackingBaseUrl: 'https://track.example',
      legal,
    })

    expect(result.sent).toEqual([])
    expect(result.refused).toHaveLength(1)
    expect(fetchImpl).not.toHaveBeenCalledWith(
      'https://api.brevo.com/v3/emailCampaigns/99/sendNow',
      expect.anything(),
    )
  })

  it('maps campaignStats from the nested statistics.globalStats object', async () => {
    const fetchImpl = createFetchMock({
      '/emailCampaigns/55': () =>
        new Response(
          JSON.stringify({
            statistics: {
              globalStats: {
                uniqueViews: 10,
                uniqueClicks: 4,
                hardBounces: 2,
                unsubscriptions: 1,
              },
            },
          }),
          { status: 200 },
        ),
    })

    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    const stats = await adapter.campaignStats('55')

    expect(stats).toEqual({ opens: 10, clicks: 4, bounces: 2, unsubs: 1 })
    // Proves the base campaign GET with query filters, NOT a /statistics subpath.
    const calledUrl = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as string
    expect(calledUrl).toContain('/emailCampaigns/55')
    expect(calledUrl).toContain('statistics=globalStats')
    expect(calledUrl).not.toContain('/55/statistics')
  })

  it('maps syncSuppression to real blockedAt + structured reason.code', async () => {
    const fetchImpl = createFetchMock({
      '/smtp/blockedContacts': () =>
        new Response(
          JSON.stringify({
            contacts: [
              {
                email: 'a@x.com',
                blockedAt: '2026-03-01T12:00:00.000Z',
                reason: { code: 'hardBounce', message: '...' },
              },
              {
                email: 'b@x.com',
                blockedAt: '2026-03-02T00:00:00.000Z',
                reason: { code: 'unsubscribedViaEmail' },
              },
              {
                email: 'c@x.com',
                blockedAt: '2026-03-03T00:00:00.000Z',
                reason: { code: 'adminBlocked' },
              },
            ],
          }),
          { status: 200 },
        ),
    })

    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      fetchImpl,
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    const entries = await adapter.syncSuppression()

    // Real suppressedAt passthrough — the blockedAt values, NOT epoch-0 (1970).
    expect(entries.map((e) => e.suppressedAt)).toEqual([
      '2026-03-01T12:00:00.000Z',
      '2026-03-02T00:00:00.000Z',
      '2026-03-03T00:00:00.000Z',
    ])
    expect(entries.map((e) => e.reason)).toEqual(['bounce', 'unsubscribe', 'manual'])
  })

  it('exposes supports flags that gate capability subpaths', () => {
    const adapter = makeBrevoAdapter({
      apiKey: 'key',
      senderEmail: 'news@example.com',
      complianceStore: createMemoryComplianceStore(),
    })

    expect(adapter.supports).toEqual({
      segments: true,
      scheduling: true,
      automation: true,
    })
  })
})
