import { eq } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '../../db/src/postgres/pglite.js'
import {
  ImmutableFieldError,
  isImmutableFieldError,
  isProductValidationError,
  ProductValidationError,
} from './errors.js'
import { pushSchema } from './migrate.js'
import { getProductById } from './reads.js'
import { catalogSchema, product, variant, variantPrice } from './schema.js'
import { setProductStatus, setVariantPrices, upsertProduct } from './writes.js'

async function freshDb() {
  const db = createPgliteClient({ schema: catalogSchema })
  await pushSchema(db)
  return db
}

describe('commerce-catalog writes', () => {
  it('creates then updates a product via upsert', async () => {
    const db = await freshDb()

    const created = await upsertProduct(db, {
      kind: 'physical',
      slug: 'widget',
      title: 'Widget v1',
      description: 'First',
    })
    expect(created.slug).toBe('widget')
    expect(created.status).toBe('draft')
    expect(created.variants).toEqual([])

    const updated = await upsertProduct(db, {
      id: created.id,
      kind: 'physical',
      slug: 'widget',
      title: 'Widget v2',
      description: 'Second',
      status: 'active',
    })
    expect(updated.title).toBe('Widget v2')
    expect(updated.status).toBe('active')
  })

  it('preserves existing description on update when the optional field is omitted (PATCH-on-omit, no wipe)', async () => {
    const db = await freshDb()

    const created = await upsertProduct(db, {
      kind: 'physical',
      slug: 'kept-desc',
      title: 'Has description',
      description: 'Original description',
    })
    expect(created.description).toBe('Original description')

    // Admin re-saves the product without re-supplying description (the data-loss footgun).
    const updated = await upsertProduct(db, {
      id: created.id,
      kind: 'physical',
      slug: 'kept-desc',
      title: 'Title changed',
      status: 'active',
    })

    expect(updated.title).toBe('Title changed')
    expect(updated.status).toBe('active')
    // Omitted description must KEEP the existing value, never reset to null.
    expect(updated.description).toBe('Original description')

    // Confirm at the raw column too — not just the hydrated read model.
    const [raw] = await db.select().from(product).where(eq(product.id, created.id))
    expect(raw?.description).toBe('Original description')
  })

  it('rejects duplicate slug under the same vendor with ProductValidationError', async () => {
    const db = await freshDb()
    const vendorId = '11111111-1111-4111-8111-111111111111'

    await upsertProduct(db, { kind: 'digital', slug: 'album', title: 'A', vendorId })

    await expect(upsertProduct(db, { kind: 'digital', slug: 'album', title: 'B', vendorId })).rejects.toThrow(
      ProductValidationError,
    )
    try {
      await upsertProduct(db, { kind: 'digital', slug: 'album', title: 'B', vendorId })
    } catch (e) {
      expect(isProductValidationError(e)).toBe(true)
      expect((e as ProductValidationError).field).toBe('slug')
      expect((e as ProductValidationError).detail).toBe('duplicate')
    }
  })

  it('allows the same slug under different vendorId namespaces', async () => {
    const db = await freshDb()
    const vendorA = '11111111-1111-4111-8111-111111111111'
    const vendorB = '22222222-2222-4222-8222-222222222222'

    const a = await upsertProduct(db, { kind: 'physical', slug: 'tee', title: 'Vendor A', vendorId: vendorA })
    const b = await upsertProduct(db, { kind: 'physical', slug: 'tee', title: 'Vendor B', vendorId: vendorB })

    expect(a.slug).toBe('tee')
    expect(b.slug).toBe('tee')
    expect(a.vendorId).toBe(vendorA)
    expect(b.vendorId).toBe(vendorB)
  })

  it('rejects an explicit kind change on an existing product with ImmutableFieldError(kind)', async () => {
    const db = await freshDb()
    const created = await upsertProduct(db, { kind: 'physical', slug: 'immutable-kind', title: 'Box' })

    await expect(
      upsertProduct(db, { id: created.id, kind: 'digital', slug: 'immutable-kind', title: 'Box' }),
    ).rejects.toThrow(ImmutableFieldError)

    try {
      await upsertProduct(db, { id: created.id, kind: 'digital', slug: 'immutable-kind', title: 'Box' })
      throw new Error('expected ImmutableFieldError')
    } catch (e) {
      expect(isImmutableFieldError(e)).toBe(true)
      expect((e as ImmutableFieldError).field).toBe('kind')
    }
  })

  it('rejects an explicit vendorId change on an existing product with ImmutableFieldError(vendorId)', async () => {
    const db = await freshDb()
    const vendorA = '11111111-1111-4111-8111-111111111111'
    const vendorB = '22222222-2222-4222-8222-222222222222'
    const created = await upsertProduct(db, {
      kind: 'physical',
      slug: 'immutable-vendor',
      title: 'Owned',
      vendorId: vendorA,
    })

    await expect(
      upsertProduct(db, {
        id: created.id,
        kind: 'physical',
        slug: 'immutable-vendor',
        title: 'Owned',
        vendorId: vendorB,
      }),
    ).rejects.toThrow(ImmutableFieldError)

    try {
      await upsertProduct(db, {
        id: created.id,
        kind: 'physical',
        slug: 'immutable-vendor',
        title: 'Owned',
        vendorId: vendorB,
      })
      throw new Error('expected ImmutableFieldError')
    } catch (e) {
      expect(isImmutableFieldError(e)).toBe(true)
      expect((e as ImmutableFieldError).field).toBe('vendorId')
    }

    // The frozen owner must NOT have been re-scoped by the rejected write.
    const [raw] = await db.select().from(product).where(eq(product.id, created.id))
    expect(raw?.vendorId).toBe(vendorA)
  })

  it('allows a same-value vendorId resubmit on update (idempotent full-object resubmit, no throw)', async () => {
    const db = await freshDb()
    const vendorA = '11111111-1111-4111-8111-111111111111'
    const created = await upsertProduct(db, {
      kind: 'physical',
      slug: 'resubmit-vendor',
      title: 'Owned v1',
      vendorId: vendorA,
    })

    // A PATCH/PUT/agent client re-sends the full object including the unchanged vendorId.
    const updated = await upsertProduct(db, {
      id: created.id,
      kind: 'physical',
      slug: 'resubmit-vendor',
      title: 'Owned v2',
      vendorId: vendorA,
    })

    expect(updated.title).toBe('Owned v2')
    expect(updated.vendorId).toBe(vendorA)
  })

  it('preserves the existing vendorId on update when vendorId is omitted (never silently re-scopes to null)', async () => {
    const db = await freshDb()
    const vendorA = '11111111-1111-4111-8111-111111111111'
    const created = await upsertProduct(db, {
      kind: 'physical',
      slug: 'omit-vendor',
      title: 'Owned',
      vendorId: vendorA,
    })

    // The tenancy footgun: omitting vendorId must NOT re-scope the product to the platform tenant (null).
    const updated = await upsertProduct(db, {
      id: created.id,
      kind: 'physical',
      slug: 'omit-vendor',
      title: 'Owned renamed',
      status: 'active',
    })

    expect(updated.title).toBe('Owned renamed')
    expect(updated.vendorId).toBe(vendorA)

    // Confirm at the raw column too — the ownership key must be untouched.
    const [raw] = await db.select().from(product).where(eq(product.id, created.id))
    expect(raw?.vendorId).toBe(vendorA)
  })

  it('rejects invalid slug shape with ProductValidationError', async () => {
    const db = await freshDb()
    await expect(upsertProduct(db, { kind: 'voucher', slug: 'Bad_Slug', title: 'Nope' })).rejects.toThrow(
      ProductValidationError,
    )
  })

  it('replace-sets variant prices (old currency removed, not merged)', async () => {
    const db = await freshDb()
    const created = await upsertProduct(db, { kind: 'physical', slug: 'priced', title: 'Priced' })
    const [v] = await db.insert(variant).values({ productId: created.id, sku: 'SKU-1' }).returning()

    await setVariantPrices(db, v!.id, [
      { currency: 'USD', amount: 1000n, priceMode: 'exclusive' },
      { currency: 'EUR', amount: 900n, priceMode: 'exclusive' },
    ])

    await setVariantPrices(db, v!.id, [{ currency: 'ILS', amount: 3500n, priceMode: 'inclusive' }])

    const rows = await db.select().from(variantPrice).where(eq(variantPrice.variantId, v!.id))
    expect(rows).toHaveLength(1)
    expect(rows[0]?.currency).toBe('ILS')
    expect(rows[0]?.amount).toBe(3500n)

    const loaded = await getProductById(db, created.id, { audience: 'admin' })
    expect(loaded?.variants[0]?.prices).toEqual([{ currency: 'ILS', amount: 3500n, priceMode: 'inclusive' }])
  })

  it('transitions product status draft to active to archived', async () => {
    const db = await freshDb()
    const created = await upsertProduct(db, { kind: 'voucher', slug: 'gift', title: 'Gift' })
    expect(created.status).toBe('draft')

    await setProductStatus(db, created.id, 'active')
    let row = await getProductById(db, created.id, { audience: 'admin' })
    expect(row?.status).toBe('active')

    await setProductStatus(db, created.id, 'archived')
    row = await getProductById(db, created.id, { audience: 'admin' })
    expect(row?.status).toBe('archived')

    const [raw] = await db.select().from(product).where(eq(product.id, created.id))
    expect(raw?.status).toBe('archived')
  })
})
