import { Miniflare } from 'miniflare'
import type { D1Database } from '@cloudflare/workers-types'
import { describe, expect, it } from 'vitest'
import {
  makeInMemoryRecordStore,
  type RecordStore,
  type Stored,
} from '@platform-modules/record-store'
import {
  makeD1RecordStore,
  recordStoreD1TableSql,
} from '@platform-modules/record-store/d1'
import { makeCaptureAdapter } from '@platform-modules/mail/capture'
import { makeFakeMailAdapter } from '@platform-modules/mail/testing'
import type { CapturedMail } from '@platform-modules/mail/capture'
import type { MailMessage } from '@platform-modules/mail'

const TABLE = 'consumer_record_store'
const D1_BINDING = 'DB'
const D1_DATABASE_ID = '00000000-0000-4000-8000-000000000003'

/** Minute offsets per append — indices 2–4 share offset 2 (same-createdAtMs batch). */
const MINUTE_OFFSETS = [0, 1, 2, 2, 2, 3, 4, 5, 6, 7]

function formatNonCanonicalIso(offsetMinutes: number): string {
  const totalMinutes = offsetMinutes
  const hours = Math.floor(totalMinutes / 60)
  const mins = totalMinutes % 60
  return `2026-01-01T${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00+02:00`
}

function createDeterministicGenerators(): { now: () => string; makeId: () => string } {
  let idCounter = 0
  let nowCallCount = 0

  return {
    now: () => {
      const offset = MINUTE_OFFSETS[nowCallCount] ?? nowCallCount
      nowCallCount++
      return formatNonCanonicalIso(offset)
    },
    makeId: () => {
      idCounter++
      return `id-${idCounter}`
    },
  }
}

const parityMessages: MailMessage[] = [
  {
    from: 'sender@example.com',
    to: ['alice@example.com', 'bob@example.com'],
    subject: 'Welcome aboard',
    html: '<p>Hello team</p>',
    text: 'Hello team',
    headers: { 'X-Campaign': 'onboarding' },
    tags: { env: 'test' },
    idempotencyKey: 'key-1',
  },
  {
    from: 'noreply@example.com',
    to: 'solo@example.com',
    cc: ['cc1@example.com', 'cc2@example.com'],
    subject: 'CC exercise',
    text: 'Plain only',
  },
  {
    from: 'batch@example.com',
    to: 'tie-a@example.com',
    subject: 'Same timestamp batch A',
    html: '<p>A</p>',
  },
  {
    from: 'batch@example.com',
    to: 'tie-b@example.com',
    subject: 'Same timestamp batch B',
    html: '<p>B</p>',
    replyTo: 'reply@example.com',
  },
  {
    from: 'batch@example.com',
    to: ['tie-c@example.com'],
    subject: 'Same timestamp batch C',
    text: 'C',
    headers: { 'X-Batch': '3' },
  },
  {
    from: 'later@example.com',
    to: 'after-batch@example.com',
    subject: 'After the batch',
    html: '<p>Later</p>',
    tags: { phase: 'post-batch' },
  },
  {
    from: 'rich@example.com',
    to: 'rich@example.com',
    cc: 'cc-single@example.com',
    bcc: ['bcc@example.com'],
    subject: 'Full shape',
    html: '<p>Rich</p>',
    text: 'Rich',
    idempotencyKey: 'key-7',
  },
  {
    from: 'eight@example.com',
    to: 'eight@example.com',
    subject: 'Message eight',
  },
  {
    from: 'nine@example.com',
    to: 'nine@example.com',
    subject: 'Message nine',
    tags: { n: '9' },
  },
  {
    from: 'ten@example.com',
    to: 'ten@example.com',
    subject: 'Message ten',
    headers: { 'X-Last': 'true' },
  },
]

let sharedD1: D1Database | undefined
let miniflare: Miniflare | undefined

async function getTestD1(): Promise<D1Database> {
  if (sharedD1 === undefined) {
    miniflare = new Miniflare({
      modules: true,
      script: `
        export default {
          async fetch() {
            return new Response("ok");
          },
        }
      `,
      d1Databases: { [D1_BINDING]: D1_DATABASE_ID },
    })
    sharedD1 = await miniflare.getD1Database(D1_BINDING)
    await sharedD1.exec(recordStoreD1TableSql(TABLE))
  }
  return sharedD1
}

async function resetD1Table(d1: D1Database): Promise<void> {
  await d1.prepare(`DELETE FROM ${TABLE}`).run()
  await d1.prepare(`DELETE FROM sqlite_sequence WHERE name = ?`).bind(TABLE).run()
}

async function paginateToExhaustion<T>(
  store: RecordStore<T>,
  pageLimit = 2,
): Promise<Stored<T>[]> {
  const all: Stored<T>[] = []
  let cursor: string | undefined

  for (;;) {
    const page = await store.list({ limit: pageLimit, cursor })
    all.push(...page.items)
    if (page.nextCursor === null) break
    cursor = page.nextCursor
  }

  return all
}

function assertNewestFirst<T>(items: Stored<T>[]): void {
  for (let i = 1; i < items.length; i++) {
    const prev = Date.parse(items[i - 1]!.createdAt)
    const curr = Date.parse(items[i]!.createdAt)
    expect(prev).toBeGreaterThanOrEqual(curr)
  }
}

describe('record-store consumer fixture (Gate 3 — swap-parity harness)', () => {
  it('broken-export smoke: consumer subpaths import and makers are callable', () => {
    expect(typeof makeInMemoryRecordStore).toBe('function')
    expect(typeof makeD1RecordStore).toBe('function')
    expect(typeof recordStoreD1TableSql).toBe('function')
    expect(typeof makeCaptureAdapter).toBe('function')
    expect(typeof makeFakeMailAdapter).toBe('function')
  })

  it('makeFakeMailAdapter round-trip via store.list()', async () => {
    const adapter = makeFakeMailAdapter()
    const msg: MailMessage = {
      from: 'from@example.com',
      to: 'to@example.com',
      subject: 'Smoke',
      html: '<p>Hi</p>',
    }

    await adapter.send(msg)

    const page = await adapter.store.list()
    expect(page.items).toHaveLength(1)
    expect(page.items[0]!.record).toEqual(msg)
  })

  it('mail/capture over in-memory ≡ local-D1 (paginate-to-exhaustion deep-equal)', async () => {
    const inMemGens = createDeterministicGenerators()
    const inMemStore = makeInMemoryRecordStore<CapturedMail>(inMemGens)
    const inMemAdapter = makeCaptureAdapter({ store: inMemStore, provider: 'capture-test' })

    const d1Gens = createDeterministicGenerators()
    const d1 = await getTestD1()
    await resetD1Table(d1)
    const d1Store = makeD1RecordStore<CapturedMail>(d1, TABLE, d1Gens)
    const d1Adapter = makeCaptureAdapter({ store: d1Store, provider: 'capture-test' })

    for (const msg of parityMessages) {
      await inMemAdapter.send(msg)
      await d1Adapter.send(msg)
    }

    const inMemItems = await paginateToExhaustion(inMemStore, 2)
    const d1Items = await paginateToExhaustion(d1Store, 2)

    expect(inMemItems).toHaveLength(parityMessages.length)
    expect(d1Items).toHaveLength(parityMessages.length)

    assertNewestFirst(inMemItems)
    assertNewestFirst(d1Items)

    const batchSubjects = [
      'Same timestamp batch A',
      'Same timestamp batch B',
      'Same timestamp batch C',
    ]
    const sameCreatedAtBatch = inMemItems.filter((item) =>
      batchSubjects.includes(item.record.subject),
    )
    expect(sameCreatedAtBatch).toHaveLength(3)
    expect(new Set(sameCreatedAtBatch.map((item) => item.createdAt)).size).toBe(1)

    expect(inMemItems).toEqual(d1Items)
  }, 30_000)
})
