import { sql } from 'drizzle-orm'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { TransactionalDatabase } from '@platform-modules/db'
import type { InventorySchema } from './schema.js'
import { isInventoryValidationError } from './errors.js'
import { startPg } from './internal/pg-harness.js'
import { postIssue } from './post-issue.js'
import { postReceipt } from './post-receipt.js'
import { recordCount } from './record-count.js'
import {
  AVG_ITEM_ID,
  FIFO_ITEM_ID,
  LOCATION_A,
  LOCATION_B,
  NOW,
  TENANT_ID,
  freshDb,
  firstRows,
  seedItem,
  seedLocation,
} from './test-helpers.js'

const CONCURRENCY_ITEM_ID = '10000000-0000-4000-8000-000000000006'

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

describe('recordCount', () => {
  it('posts positive and negative count variances and skips zero variance lines', async () => {
    const db = await freshDb()
    await seedLocation(db, LOCATION_A)
    await seedItem(db, AVG_ITEM_ID, 'weighted_average')
    await seedItem(db, FIFO_ITEM_ID, 'fifo')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: AVG_ITEM_ID,
      locationId: LOCATION_A,
      qty: 5,
      unitCost: 8,
      occurredAt: NOW,
    })
    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: FIFO_ITEM_ID,
      locationId: LOCATION_A,
      qty: 7,
      unitCost: 4,
      occurredAt: NOW,
    })

    const results = await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_A,
      holderRef: 'count-1',
      lines: [
        { itemId: AVG_ITEM_ID, countedQty: 7 },
        { itemId: FIFO_ITEM_ID, countedQty: 7 },
        { itemId: FIFO_ITEM_ID, countedQty: 4 },
      ],
      occurredAt: new Date('2026-07-04T13:00:00.000Z'),
    })

    expect(results).toHaveLength(2)
    expect(results[0]?.variance).toBe('2')
    expect(results[1]?.variance).toBe('-3')
  })

  it('replaying the same holderRef returns the existing count result without adding movements', async () => {
    const db = await freshDb()
    await seedLocation(db, LOCATION_A)
    await seedItem(db, AVG_ITEM_ID, 'weighted_average')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: AVG_ITEM_ID,
      locationId: LOCATION_A,
      qty: 5,
      unitCost: 8,
      holderRef: 'count-receipt',
      occurredAt: NOW,
    })

    const beforeCount = firstRows(await db.execute(sql`
      SELECT COUNT(*)::int AS count
      FROM stock_movement
      WHERE tenant_id = ${TENANT_ID}::uuid
    `))

    const first = await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_A,
      holderRef: 'count-idempotent',
      lines: [{ itemId: AVG_ITEM_ID, countedQty: 7 }],
      occurredAt: NOW,
    })
    const replay = await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_A,
      holderRef: 'count-idempotent',
      lines: [{ itemId: AVG_ITEM_ID, countedQty: 7 }],
      occurredAt: NOW,
    })

    expect(replay).toHaveLength(1)
    expect(replay[0]?.movement.id).toBe(first[0]?.movement.id)

    const afterCount = firstRows(await db.execute(sql`
      SELECT COUNT(*)::int AS count
      FROM stock_movement
      WHERE tenant_id = ${TENANT_ID}::uuid
    `))
    expect(Number(afterCount[0]?.count ?? 0)).toBe(Number(beforeCount[0]?.count ?? 0) + 1)
  })

  it('rejects a replay of the same holderRef with a different countedQty', async () => {
    const db = await freshDb()
    await seedLocation(db, LOCATION_A)
    await seedItem(db, AVG_ITEM_ID, 'weighted_average')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: AVG_ITEM_ID,
      locationId: LOCATION_A,
      qty: 5,
      unitCost: 8,
      occurredAt: NOW,
    })

    await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_A,
      holderRef: 'count-mismatch',
      lines: [{ itemId: AVG_ITEM_ID, countedQty: 7 }],
      occurredAt: NOW,
    })

    await expect(
      recordCount(db, {
        tenantId: TENANT_ID,
        locationId: LOCATION_A,
        holderRef: 'count-mismatch',
        lines: [{ itemId: AVG_ITEM_ID, countedQty: 9 }],
        occurredAt: NOW,
      }),
    ).rejects.toSatisfy((error) => isInventoryValidationError(error))
  })

  it('rejects a negative countedQty', async () => {
    const db = await freshDb()
    await seedLocation(db, LOCATION_A)
    await seedItem(db, AVG_ITEM_ID, 'weighted_average')

    await expect(
      recordCount(db, {
        tenantId: TENANT_ID,
        locationId: LOCATION_A,
        holderRef: 'count-negative',
        lines: [{ itemId: AVG_ITEM_ID, countedQty: -1 }],
        occurredAt: NOW,
      }),
    ).rejects.toSatisfy((error) => isInventoryValidationError(error))
  })

  it('scopes idempotency to location — same holderRef+item at a different location posts its own variance', async () => {
    const db = await freshDb()
    await seedLocation(db, LOCATION_A)
    await seedLocation(db, LOCATION_B)
    await seedItem(db, AVG_ITEM_ID, 'weighted_average')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: AVG_ITEM_ID,
      locationId: LOCATION_A,
      qty: 5,
      unitCost: 8,
      occurredAt: NOW,
    })
    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: AVG_ITEM_ID,
      locationId: LOCATION_B,
      qty: 3,
      unitCost: 8,
      occurredAt: NOW,
    })

    const atA = await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_A,
      holderRef: 'count-shared-ref',
      lines: [{ itemId: AVG_ITEM_ID, countedQty: 7 }],
      occurredAt: NOW,
    })
    const atB = await recordCount(db, {
      tenantId: TENANT_ID,
      locationId: LOCATION_B,
      holderRef: 'count-shared-ref',
      lines: [{ itemId: AVG_ITEM_ID, countedQty: 6 }],
      occurredAt: NOW,
    })

    expect(atA).toHaveLength(1)
    expect(atB).toHaveLength(1)
    expect(atA[0]?.movement.id).not.toBe(atB[0]?.movement.id)
    expect(atA[0]?.variance).toBe('2')
    expect(atB[0]?.variance).toBe('3')
  })
})

describe('recordCount concurrency (real Postgres)', () => {
  let db: TransactionalDatabase<InventorySchema>
  let stop: (() => Promise<void>) | undefined

  beforeAll(async () => {
    const pg = await startPg()
    db = pg.db
    stop = pg.stop
  }, 120_000)

  afterAll(async () => {
    await stop?.()
  }, 30_000)

  it('serializes count variance against a concurrent postIssue on the same item and location', async () => {
    await seedLocation(db, LOCATION_A)
    await seedItem(db, CONCURRENCY_ITEM_ID, 'weighted_average')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: CONCURRENCY_ITEM_ID,
      locationId: LOCATION_A,
      qty: 5,
      unitCost: 10,
      holderRef: 'count-concurrency-receipt',
      occurredAt: NOW,
    })

    let resolveHold!: () => void
    const hold = new Promise<void>((resolve) => {
      resolveHold = resolve
    })

    let countHasLock!: () => void
    const countLocked = new Promise<void>((resolve) => {
      countHasLock = resolve
    })

    const countTx = db.transaction(async (tx) => {
      const results = await recordCount(tx, {
        tenantId: TENANT_ID,
        locationId: LOCATION_A,
        holderRef: 'count-concurrency',
        lines: [{ itemId: CONCURRENCY_ITEM_ID, countedQty: 4 }],
        occurredAt: NOW,
      })
      countHasLock()
      await hold
      return results
    })

    await countLocked

    const issueTx = postIssue(db, {
      tenantId: TENANT_ID,
      itemId: CONCURRENCY_ITEM_ID,
      locationId: LOCATION_A,
      qty: 2,
      holderRef: 'count-concurrency-issue',
      occurredAt: NOW,
      method: 'weighted_average',
      allowNegative: false,
    })

    await sleep(400)
    resolveHold()

    const [countResults, issueResult] = await Promise.all([countTx, issueTx])
    expect(countResults).toHaveLength(1)
    expect(countResults[0]?.variance).toBe('-1')

    const qtyRes = await db.execute(sql`
      SELECT COALESCE(SUM(qty_delta), 0)::numeric AS qty
      FROM stock_movement
      WHERE tenant_id = ${TENANT_ID}::uuid
        AND item_id = ${CONCURRENCY_ITEM_ID}::uuid
        AND location_id = ${LOCATION_A}::uuid
    `)
    expect(Number(firstRows(qtyRes)[0]?.qty ?? 0)).toBe(2)
    expect(Number(issueResult.movement.qtyDelta)).toBe(-2)
  })

  it('returns the existing count movement for a concurrent holderRef replay', async () => {
    const replayItemId = '10000000-0000-4000-8000-000000000007'
    const replayLocationId = '10000000-0000-4000-8000-000000000008'
    await seedLocation(db, replayLocationId)
    await seedItem(db, replayItemId, 'weighted_average')

    await postReceipt(db, {
      tenantId: TENANT_ID,
      itemId: replayItemId,
      locationId: replayLocationId,
      qty: 5,
      unitCost: 10,
      holderRef: 'count-replay-concurrency-receipt',
      occurredAt: NOW,
    })

    let releaseFirst!: () => void
    const firstCanFinish = new Promise<void>((resolve) => {
      releaseFirst = resolve
    })

    const firstCount = db.transaction(async (tx) => {
      const results = await recordCount(tx, {
        tenantId: TENANT_ID,
        locationId: replayLocationId,
        holderRef: 'count-replay-concurrency',
        lines: [{ itemId: replayItemId, countedQty: 4 }],
        occurredAt: NOW,
      })
      await firstCanFinish
      return results
    })

    await sleep(100)

    const replayCount = recordCount(db, {
      tenantId: TENANT_ID,
      locationId: replayLocationId,
      holderRef: 'count-replay-concurrency',
      lines: [{ itemId: replayItemId, countedQty: 4 }],
      occurredAt: NOW,
    })

    await sleep(300)
    releaseFirst()

    const [first, replay] = await Promise.all([firstCount, replayCount])
    expect(first).toHaveLength(1)
    expect(replay).toHaveLength(1)
    expect(replay[0]?.movement.id).toBe(first[0]?.movement.id)
  })
})
