import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { and, eq, sql } from 'drizzle-orm'
import {
  RateLimitError,
  setAdapterFactories,
  type AIAdapterFactory,
  type AIRequest,
} from '@platform-modules/ai'
import {
  extractMessageText,
  makeMockAdapter,
  resolveMockDirective,
  throwForMockDirective,
} from '@platform-modules/ai/mock'
import type { TransactionalDatabase } from '@platform-modules/db'
import { pushSchema } from '@platform-modules/i18n-content/migrate'
import { upsertLanguage } from '@platform-modules/i18n-content'
import { setTranslation, translationValue, type I18nContentSchema } from '@platform-modules/i18n-content'
import { isI18nTranslatorError, SourceUnavailableError, UnknownLocaleError } from './errors.js'
import { hashSource } from './hash.js'
import type { SourceTextProvider } from './model.js'
import { makePgHarness } from './pg-harness.js'
import { translateEntity } from './translate.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 },
      }
    },
  }
}

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(fields: Record<string, string>, locale = 'en'): SourceTextProvider {
  return {
    async getSourceText() {
      return { locale, fields }
    },
  }
}

describe('translateEntity', () => {
  it('never translates into the source locale', async () => {
    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title', 'description'],
      targetLocales: ['en', 'fr'],
      source: makeProvider({
        title: 'Hello',
        description: 'World',
      }),
    })

    const en = result.results.find((r) => r.locale === 'en')
    const fr = result.results.find((r) => r.locale === 'fr')
    expect(en?.fields.every((f) => f.outcome === 'SKIPPED')).toBe(true)
    expect(fr?.fields.every((f) => f.outcome === 'OK')).toBe(true)

    const titleRow = await readRow('deal', 'd1', 'title', 'fr')
    expect(titleRow?.value).toBe('[fr] Hello')
    expect(titleRow?.status).toBe('OK')
  })

  it('skipUnchanged skips when stored sourceHash matches', async () => {
    const hash = await hashSource('Hello')
    await setTranslation(db, {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'Existing',
      sourceHash: hash,
    })

    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Hello' }),
    })

    expect(result.results[0]?.fields[0]?.outcome).toBe('SKIPPED')
    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.value).toBe('Existing')
  })

  it('skipUnchanged re-translates when source hash changes', async () => {
    const oldHash = await hashSource('Hello')
    await setTranslation(db, {
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'Existing',
      sourceHash: oldHash,
    })

    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Changed' }),
    })

    expect(result.results[0]?.fields[0]?.outcome).toBe('OK')
    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.value).toBe('[fr] Changed')
  })

  it('re-translates a STALE row at the current source hash', async () => {
    const hash = await hashSource('Hello')
    await db.insert(translationValue).values({
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: 'stale value',
      sourceHash: hash,
      status: 'STALE',
      manualOverride: false,
      modelId: null,
    })

    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Hello' }),
    })

    expect(result.results[0]?.fields[0]?.outcome).toBe('OK')
    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.value).toBe('[fr] Hello')
    expect(row?.status).toBe('OK')
  })

  it('re-translates a FAILED row at the current source hash', async () => {
    const hash = await hashSource('Hello')
    await db.insert(translationValue).values({
      entityType: 'deal',
      entityId: 'd1',
      fieldKey: 'title',
      locale: 'fr',
      value: '',
      sourceHash: hash,
      status: 'FAILED',
      manualOverride: false,
      modelId: null,
    })

    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Hello' }),
    })

    expect(result.results[0]?.fields[0]?.outcome).toBe('OK')
    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.value).toBe('[fr] Hello')
    expect(row?.status).toBe('OK')
  })

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

    await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Machine source' }),
    })

    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 when ai returns an error', async () => {
    const result = await translateEntity(db, {
      entityType: 'deal',
      entityId: 'd1',
      fields: ['title'],
      targetLocales: ['fr'],
      source: makeProvider({ title: 'Hello' }),
      modelId: 'TEST-ERROR',
    })

    expect(result.results[0]?.fields[0]?.outcome).toBe('FAILED')
    const row = await readRow('deal', 'd1', 'title', 'fr')
    expect(row?.status).toBe('FAILED')
    expect(row?.value).toBe('')
  })

  it('throws UnknownLocaleError for inactive target locale', async () => {
    await expect(
      translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['de'],
        source: makeProvider({ title: 'Hello' }),
      }),
    ).rejects.toSatisfy((err: unknown) => {
      expect(isI18nTranslatorError(err)).toBe(true)
      expect(err).toBeInstanceOf(UnknownLocaleError)
      return true
    })
  })

  it('throws SourceUnavailableError when provider.getSourceText throws', async () => {
    const source: SourceTextProvider = {
      async getSourceText() {
        throw new Error('db down')
      },
    }

    await expect(
      translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['fr'],
        source,
      }),
    ).rejects.toBeInstanceOf(SourceUnavailableError)
  })

  it('passes provider default model to adapter when modelId is omitted', async () => {
    let capturedModel: string | undefined

    setAdapterFactories({
      'openai-compat': () => {
        const adapter = createTranslationMockAdapter()
        return {
          ...adapter,
          async call(req: AIRequest) {
            capturedModel = req.model
            return adapter.call(req)
          },
        }
      },
    })

    try {
      await translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['fr'],
        source: makeProvider({ title: 'Hello' }),
      })

      expect(capturedModel).toBe('gpt-4o-mini')
      expect(capturedModel).not.toBe('translation')
    } finally {
      setAdapterFactories({ 'openai-compat': () => createTranslationMockAdapter() })
    }
  })

  it('parses a fenced ```json response without marking FAILED', async () => {
    setAdapterFactories({
      'openai-compat': () => {
        const adapter = createTranslationMockAdapter()
        return {
          ...adapter,
          async call(req: AIRequest) {
            const base = await adapter.call(req)
            // Wrap the JSON in markdown fences, as many models do
            return { ...base, content: '```json\n' + base.content + '\n```' }
          },
        }
      },
    })

    try {
      const result = await translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['fr'],
        source: makeProvider({ title: 'Hello' }),
      })

      expect(result.results[0]?.fields[0]?.outcome).toBe('OK')
      const row = await readRow('deal', 'd1', 'title', 'fr')
      expect(row?.value).toBe('[fr] Hello')
    } finally {
      setAdapterFactories({ 'openai-compat': () => createTranslationMockAdapter() })
    }
  })

  it('fallback provider receives its own default model, not the caller modelId', async () => {
    let anthropicReceivedModel: string | undefined

    setAdapterFactories({
      'openai-compat': () => ({
        provider: 'openai-compat',
        async call() {
          throw new RateLimitError('rate limited', 'openai-compat')
        },
      }),
      anthropic: () => {
        const adapter = createTranslationMockAdapter()
        return {
          ...adapter,
          async call(req: AIRequest) {
            anthropicReceivedModel = req.model
            return adapter.call(req)
          },
        }
      },
    })

    try {
      await translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['fr'],
        source: makeProvider({ title: 'Hello' }),
        modelId: 'gpt-4o-mini', // belongs to openai-compat
      })

      // The anthropic fallback must receive its own provider default, not the openai model id
      expect(anthropicReceivedModel).toBe('claude-haiku-4-5')
      expect(anthropicReceivedModel).not.toBe('gpt-4o-mini')
    } finally {
      setAdapterFactories({
        'openai-compat': () => createTranslationMockAdapter(),
        anthropic: undefined as AIAdapterFactory | undefined,
      })
    }
  })

  it('falls back to the next provider when the first throws RateLimitError', async () => {
    setAdapterFactories({
      'openai-compat': () => ({
        provider: 'openai-compat',
        async call() {
          throw new RateLimitError('rate limited', 'openai-compat')
        },
      }),
      anthropic: () => createTranslationMockAdapter(),
    })

    try {
      const result = await translateEntity(db, {
        entityType: 'deal',
        entityId: 'd1',
        fields: ['title'],
        targetLocales: ['fr'],
        source: makeProvider({ title: 'Hello' }),
      })

      expect(result.results[0]?.fields[0]?.outcome).toBe('OK')
      const row = await readRow('deal', 'd1', 'title', 'fr')
      expect(row?.value).toBe('[fr] Hello')
      expect(row?.status).toBe('OK')
    } finally {
      setAdapterFactories({
        'openai-compat': () => createTranslationMockAdapter(),
        anthropic: undefined as AIAdapterFactory | undefined,
      })
    }
  })
})
