import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { and, eq, sql } from 'drizzle-orm'
import {
  setAdapterFactories,
  type AIRequest,
} from '@platform-modules/ai'
import {
  extractMessageText,
  makeMockAdapter,
  resolveMockDirective,
  throwForMockDirective,
} from '@platform-modules/ai/mock'
import type { Querier, TransactionalDatabase } from '@platform-modules/db'
import { createJobRegistry, type IdempotencyStore } from '@platform-modules/jobs'
import { pushSchema } from '@platform-modules/i18n-content/migrate'
import {
  setTranslation,
  translationValue,
  upsertLanguage,
  type I18nContentSchema,
} from '@platform-modules/i18n-content'
import { hashSource } from './hash.js'
import type { SourceTextProvider, StaleUnit } from './model.js'
import { makePgHarness } from './pg-harness.js'
import * as translateModule from './translate.js'
import {
  createTranslationWorker,
  idempotencyKey,
  type TranslationWorkerEnv,
} from './worker.js'

function createTranslationMockAdapter() {
  const inner = makeMockAdapter()
  return {
    ...inner,
    async call(req: AIRequest) {
      const directive = resolveMockDirective(req)
      if (directive && ['ERROR', 'RATELIMIT', 'TRANSIENT', 'QUOTA', 'AUTH'].includes(directive.kind)) {
        throwForMockDirective(directive.kind, directive.arg)
      }
      const text = extractMessageText(req.messages)
      const result: Record<string, string> = {}
      for (const match of text.matchAll(/^- (\w+): (.*)$/gm)) {
        result[match[1]!] = `[fr] ${match[2]}`
      }
      return {
        content: JSON.stringify(result),
        model: req.model,
        provider: 'mock',
        usage: { promptTokens: 1, completionTokens: 1 },
      }
    },
  }
}

function createMemoryIdempotencyStore(): IdempotencyStore {
  const marks = new Set<string>()
  return {
    async seen(key) {
      return marks.has(key)
    },
    async mark(key) {
      marks.add(key)
    },
  }
}

const h = await makePgHarness()
const db = h.db as unknown as TransactionalDatabase<I18nContentSchema>

beforeAll(async () => {
  setAdapterFactories({ 'openai-compat': () => createTranslationMockAdapter() })
  await pushSchema(h.db)
})

beforeEach(async () => {
  await db.execute(sql`DELETE FROM translation_value`)
  await db.execute(sql`DELETE FROM languages`)
  await upsertLanguage(db, {
    code: 'en',
    isActive: true,
    isDefault: true,
    sortOrder: 1,
  })
  await upsertLanguage(db, {
    code: 'fr',
    isActive: true,
    sortOrder: 2,
  })
})

afterAll(async () => {
  await h.teardown()
})

async function readRow(entityType: string, entityId: string, fieldKey: string, locale: string) {
  const [row] = await db
    .select()
    .from(translationValue)
    .where(
      and(
        eq(translationValue.entityType, entityType),
        eq(translationValue.entityId, entityId),
        eq(translationValue.fieldKey, fieldKey),
        eq(translationValue.locale, locale),
      ),
    )
  return row
}

function makeProvider(
  byEntity: Record<string, Record<string, string>>,
  locale = 'en',
): SourceTextProvider {
  return {
    async getSourceText({ entityId, fields }) {
      const entityFields = byEntity[entityId]
      if (!entityFields) {
        throw new Error('source unavailable')
      }
      const out: Record<string, string> = {}
      for (const field of fields) {
        if (field in entityFields) out[field] = entityFields[field]!
      }
      return { locale, fields: out }
    },
  }
}

function makeWorker(args: {
  selectStale: (
    q: Querier<I18nContentSchema>,
    opts: { locale: string; limit: number },
  ) => Promise<readonly StaleUnit[]>
  source?: SourceTextProvider
  idempotencyStore?: IdempotencyStore
}) {
  const registry = createJobRegistry<TranslationWorkerEnv>()
  return createTranslationWorker({
    q: db,
    registry,
    source: args.source ?? makeProvider({ d1: { title: 'Hello' } }),
    idempotencyStore: args.idempotencyStore ?? createMemoryIdempotencyStore(),
    selectStale: args.selectStale,
  })
}

describe('createTranslationWorker', () => {
  it('skips already-seen stale units on repeat sweep (idempotency)', async () => {
    const hash = await hashSource('Hello')
    const unit: StaleUnit = {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      sourceHash: hash,
    }

    await db.insert(translationValue).values({
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'stale',
      sourceHash: hash,
      status: 'STALE',
      manualOverride: false,
      modelId: null,
    })

    const selectStale = vi.fn(async () => [unit])
    const translateSpy = vi.spyOn(translateModule, 'translateEntity')
    const worker = makeWorker({ selectStale })

    try {
      await worker.runSweep({ limitPerLocale: 10 })
      await worker.runSweep({ limitPerLocale: 10 })

      expect(translateSpy).toHaveBeenCalledTimes(1)
      expect(selectStale).toHaveBeenCalled()
    } finally {
      translateSpy.mockRestore()
    }
  })

  it('does not mark failed units and continues when translateEntity throws for one batch', async () => {
    const hash1 = await hashSource('Hello')
    const hash2 = await hashSource('World')
    const unitFail: StaleUnit = {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      sourceHash: hash1,
    }
    const unitOk: StaleUnit = {
      entityType: 'deal',
      entityId: 'd2',
      fieldKey: 'title',
      locale: 'fr',
      sourceHash: hash2,
    }

    await db.insert(translationValue).values([
      {
        entityType: 'deal',
        entityId: 'd1',
        fieldKey: 'title',
        locale: 'fr',
        value: '',
        sourceHash: hash1,
        status: 'FAILED',
        manualOverride: false,
        modelId: null,
      },
      {
        entityType: 'deal',
        entityId: 'd2',
        fieldKey: 'title',
        locale: 'fr',
        value: '',
        sourceHash: hash2,
        status: 'FAILED',
        manualOverride: false,
        modelId: null,
      },
    ])

    const idempotencyStore = createMemoryIdempotencyStore()
    const worker = makeWorker({
      idempotencyStore,
      source: makeProvider({
        d2: { title: 'World' },
      }),
      selectStale: async () => [unitFail, unitOk],
    })

    await expect(worker.runSweep({ limitPerLocale: 10 })).resolves.toBeUndefined()

    expect(await idempotencyStore.seen(idempotencyKey(unitFail))).toBe(false)
    expect(await idempotencyStore.seen(idempotencyKey(unitOk))).toBe(true)

    const failedRow = await readRow('deal', 'd1', 'title', 'fr')
    const okRow = await readRow('deal', 'd2', 'title', 'fr')
    expect(failedRow?.status).toBe('FAILED')
    expect(okRow?.status).toBe('OK')
    expect(okRow?.value).toBe('[fr] World')
  })

  it('does not clobber manualOverride rows during sweep', async () => {
    const hash = await hashSource('Machine source')
    await setTranslation(db, {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'Manual title',
      sourceHash: 'manual-hash',
      manualOverride: true,
    })

    const unit: StaleUnit = {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      sourceHash: hash,
    }

    const worker = makeWorker({
      source: makeProvider({ d1: { title: 'Machine source' } }),
      selectStale: async () => [unit],
    })

    await worker.runSweep({ limitPerLocale: 10 })

    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.value).toBe('Manual title')
    expect(row?.manualOverride).toBe(true)
    expect(row?.sourceHash).toBe('manual-hash')
  })

  it('persists FAILED rows with empty value when ai returns an error', async () => {
    const hash = await hashSource('Hello')
    const unit: StaleUnit = {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      sourceHash: hash,
    }

    await db.insert(translationValue).values({
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'stale',
      sourceHash: hash,
      status: 'STALE',
      manualOverride: false,
      modelId: null,
    })

    const worker = makeWorker({
      source: makeProvider({ d1: { title: 'Hello' } }),
      selectStale: async () => [unit],
    })

    setAdapterFactories({
      'openai-compat': () => ({
        provider: 'openai-compat',
        async call() {
          throw new Error('mock ai failure')
        },
      }),
    })

    try {
      await worker.runSweep({ limitPerLocale: 10 })
    } finally {
      setAdapterFactories({ 'openai-compat': () => createTranslationMockAdapter() })
    }

    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.status).toBe('FAILED')
    expect(row?.value).toBe('')
  })
})