import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

const mockCreateDb = vi.fn(() => ({}))
const mockGetCatalogShareByToken = vi.fn()
const mockGetTenantById = vi.fn()
const mockGetWhiteLabelConfig = vi.fn()
const mockGetLeadForm = vi.fn()
const mockGetProposalByPublicToken = vi.fn()
const mockRecordProposalView = vi.fn()
const mockAcceptProposalByToken = vi.fn()
const mockRejectProposalByToken = vi.fn()

vi.mock('@zync/db/queries', () => ({
  createDb: (...args: unknown[]) => mockCreateDb(...args),
  getCatalogShareByToken: (...args: unknown[]) => mockGetCatalogShareByToken(...args),
  getWhiteLabelConfig: (...args: unknown[]) => mockGetWhiteLabelConfig(...args),
  getTenantById: (...args: unknown[]) => mockGetTenantById(...args),
  getLeadForm: (...args: unknown[]) => mockGetLeadForm(...args),
  getProposalByPublicToken: (...args: unknown[]) => mockGetProposalByPublicToken(...args),
  recordProposalView: (...args: unknown[]) => mockRecordProposalView(...args),
  acceptProposalByToken: (...args: unknown[]) => mockAcceptProposalByToken(...args),
  rejectProposalByToken: (...args: unknown[]) => mockRejectProposalByToken(...args),
}))

import { catalogPublicRoute } from '../src/routes/catalog-public'
import { proposalsPublicRoute } from '../src/routes/proposals-public'

function makeApp() {
  const app = new Hono<AppEnv>()
  app.route('/api/catalog', catalogPublicRoute)
  app.route('/api/proposals', proposalsPublicRoute)
  app.route('/c', catalogPublicRoute)
  app.route('/p', proposalsPublicRoute)
  return app
}

const bindings = {
  ANALYTICS_ENGINE: { writeDataPoint: vi.fn() },
  RATE_LIMITER_CATALOG: { limit: vi.fn().mockResolvedValue({ success: true }) },
  RATE_LIMITER_PROPOSAL: { limit: vi.fn().mockResolvedValue({ success: true }) },
} as unknown as AppEnv['Bindings']

describe('marketing public route aliases', () => {
  beforeEach(() => {
    vi.clearAllMocks()

    mockGetCatalogShareByToken.mockResolvedValue({
      shareId: 'share-1',
      tenantId: 'tenant-1',
      templateId: 'template-1',
      content: { sections: [] },
      settings: { lead_form_id: null },
      utmSource: 'newsletter',
      utmMedium: 'email',
      utmCampaign: 'summer',
    })
    mockGetTenantById.mockResolvedValue({
      id: 'tenant-1',
      name: 'Acme',
      settings: { locale: 'en' },
    })
    mockGetWhiteLabelConfig.mockResolvedValue({
      logoUrl: 'logos/acme.png',
      primaryColor: '#123456',
      customDomain: 'acme.example',
    })
    mockGetLeadForm.mockResolvedValue(null)

    mockGetProposalByPublicToken.mockResolvedValue({
      id: 'proposal-1',
      tenantId: 'tenant-1',
      title: 'Proposal',
      name: 'Proposal',
      status: 'sent',
      content: { sections: [] },
      locale: 'en',
      expiresAt: null,
      totalAmount: '120.00',
      customerId: 'customer-1',
      leadId: 'lead-1',
      viewCount: 2,
      sentAt: '2026-07-01T00:00:00.000Z',
      acceptedAt: null,
      acceptedByName: null,
      rejectedAt: null,
    })
    mockRecordProposalView.mockResolvedValue({ firstView: false })
    mockAcceptProposalByToken.mockResolvedValue({ ok: true, status: 'accepted' })
    mockRejectProposalByToken.mockResolvedValue({ ok: true, status: 'rejected' })
  })

  it('serves catalog public data on both canonical and legacy paths', async () => {
    const app = makeApp()

    const canonical = await app.request('/c/token-1', undefined, bindings)
    const legacy = await app.request('/api/catalog/token-1/public', undefined, bindings)

    expect(canonical.status).toBe(200)
    expect(legacy.status).toBe(200)
    expect(mockGetCatalogShareByToken).toHaveBeenCalledTimes(2)
    expect(bindings.RATE_LIMITER_CATALOG.limit).toHaveBeenNthCalledWith(1, { key: 'token-1:unknown' })
    expect(bindings.RATE_LIMITER_CATALOG.limit).toHaveBeenNthCalledWith(2, { key: 'token-1:unknown' })
    await expect(canonical.json()).resolves.toMatchObject({
      share: { id: 'share-1' },
      tenantBranding: { tenantName: 'Acme' },
    })
  })

  it('serves proposal public data on both canonical and legacy paths', async () => {
    const app = makeApp()

    const canonical = await app.request('/p/token-1?utm_source=newsletter&utm_medium=email&utm_campaign=summer', {
      headers: { 'x-original-url': 'https://zync.is/p/token-1?utm_source=newsletter&utm_medium=email&utm_campaign=summer' },
    }, bindings)
    const legacy = await app.request('/api/proposals/token-1/public', undefined, bindings)

    expect(canonical.status).toBe(200)
    expect(legacy.status).toBe(200)
    expect(mockGetProposalByPublicToken).toHaveBeenCalledTimes(2)
    expect(mockRecordProposalView).toHaveBeenCalledTimes(2)
    expect(bindings.RATE_LIMITER_PROPOSAL.limit).toHaveBeenNthCalledWith(1, { key: 'token-1:unknown' })
    expect(bindings.RATE_LIMITER_PROPOSAL.limit).toHaveBeenNthCalledWith(2, { key: 'token-1:unknown' })
    expect(mockRecordProposalView).toHaveBeenNthCalledWith(1, {}, 'proposal-1', 'tenant-1', {
      ip: 'unknown',
      userAgent: null,
      referrer: null,
      utmSource: 'newsletter',
      utmMedium: 'email',
      utmCampaign: 'summer',
    })
    await expect(canonical.json()).resolves.toMatchObject({
      proposal: { name: 'Proposal', status: 'SENT', acceptedAt: null, rejectedAt: null },
      tenantBranding: { tenantName: 'Acme' },
    })
  })

  it('returns 409 when accepting an already accepted proposal', async () => {
    const app = makeApp()
    mockAcceptProposalByToken.mockResolvedValueOnce({ ok: false, status: 'accepted' })

    const res = await app.request('/api/proposals/token-1/accept', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({}),
    }, bindings)

    expect(res.status).toBe(409)
    await expect(res.json()).resolves.toEqual({ status: 'ACCEPTED' })
  })
})
