import { eq } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '../../db/src/postgres/pglite.js'
import {
  isImmutableFieldError,
  isNotPurchasedError,
  isReviewNotFoundError,
  isReviewValidationError,
  ReviewNotFoundError,
} from './errors.js'
import { listReviews } from './list.js'
import { pushReviewsSchema } from './migrate.js'
import { moderateReview } from './moderate.js'
import { replyToReview } from './reply.js'
import { review, reviewsSchema } from './schema.js'
import { submitReview } from './submit.js'
import {
  createFakeVerifiedPurchasePort,
  noOpAutoApproveModerationAdapter,
} from './testing/index.js'

const PRODUCT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
const USER_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
const USER_B = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'
const PURCHASE_ID_1 = 'cccccccc-cccc-4ccc-8ccc-000000000001'
const PURCHASE_ID_2 = 'dddddddd-dddd-4ddd-8ddd-000000000001'
const VENDOR_A = 'vendor-a'
const VENDOR_B = 'vendor-b'
const MISSING_REVIEW_ID = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'

async function freshDb() {
  const db = createPgliteClient({ schema: reviewsSchema })
  await pushReviewsSchema(db)
  return db
}

async function rowCount(db: Awaited<ReturnType<typeof freshDb>>) {
  const rows = await db.select().from(review)
  return rows.length
}

const purchaserPort = createFakeVerifiedPurchasePort({ purchased: true })

describe('Gate 4 — secaudit-reviews-* conformance', () => {
  it('secaudit-reviews-verified-purchase-zero-rows — port false throws and writes nothing (A4)', async () => {
    const db = await freshDb()

    await expect(
      submitReview(
        db,
        { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 4, body: 'nope', purchaseId: PURCHASE_ID_1 },
        {
          verifiedPurchase: createFakeVerifiedPurchasePort({ purchased: false }),
        },
      ),
    ).rejects.toSatisfy((e) => isNotPurchasedError(e))

    expect(await rowCount(db)).toBe(0)
  })

  it('secaudit-reviews-moderation-bypass-via-edit — approved edit re-pends and leaves public list (D5)', async () => {
    const db = await freshDb()

    const approved = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 5, body: 'clean', purchaseId: PURCHASE_ID_1 },
      {
        verifiedPurchase: purchaserPort,
        moderation: noOpAutoApproveModerationAdapter,
      },
    )
    expect(approved.status).toBe('approved')

    const edited = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 5, body: 'abuse string', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort },
    )
    expect(edited.status).toBe('pending')

    const publicPage = await listReviews(db, PRODUCT_ID, { audience: 'public' })
    expect(publicPage.items).toHaveLength(0)
  })

  it('secaudit-reviews-public-visibility-where — public list returns only approved (A6)', async () => {
    const db = await freshDb()
    const now = new Date()

    await db.insert(review).values([
      {
        productId: PRODUCT_ID,
        userId: USER_ID,
        purchaseId: PURCHASE_ID_1,
        vendorId: VENDOR_A,
        rating: 4,
        body: 'pending review',
        status: 'pending',
        createdAt: now,
        updatedAt: now,
      },
      {
        productId: PRODUCT_ID,
        userId: USER_B,
        purchaseId: PURCHASE_ID_2,
        vendorId: VENDOR_A,
        rating: 2,
        body: 'rejected review',
        status: 'rejected',
        createdAt: new Date(now.getTime() + 1),
        updatedAt: new Date(now.getTime() + 1),
      },
      {
        productId: PRODUCT_ID,
        userId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
        purchaseId: 'eeeeeeee-eeee-4eee-8eee-000000000001',
        vendorId: VENDOR_A,
        rating: 5,
        body: 'approved review',
        status: 'approved',
        createdAt: new Date(now.getTime() + 2),
        updatedAt: new Date(now.getTime() + 2),
      },
    ])

    const publicPage = await listReviews(db, PRODUCT_ID, { audience: 'public' })
    expect(publicPage.items).toHaveLength(1)
    expect(publicPage.items[0]?.status).toBe('approved')
    expect(publicPage.items[0]?.body).toBe('approved review')

    const publicVendorPage = await listReviews(db, PRODUCT_ID, {
      audience: 'public',
      vendorId: VENDOR_A,
    })
    expect(publicVendorPage.items).toHaveLength(1)
    expect(publicVendorPage.items.every((item) => item.status === 'approved')).toBe(true)
  })

  it('secaudit-reviews-cross-vendor-reply-404 — wrong vendor matches nonexistent id (A2/A5)', async () => {
    const db = await freshDb()

    const created = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 4, body: 'mine', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort },
    )

    let crossVendorError: ReviewNotFoundError | undefined
    try {
      await replyToReview(db, created.id, VENDOR_B, 'not yours')
    } catch (e) {
      if (isReviewNotFoundError(e)) {
        crossVendorError = e
      } else {
        throw e
      }
    }

    let missingError: ReviewNotFoundError | undefined
    try {
      await replyToReview(db, MISSING_REVIEW_ID, VENDOR_B, 'missing')
    } catch (e) {
      if (isReviewNotFoundError(e)) {
        missingError = e
      } else {
        throw e
      }
    }

    expect(crossVendorError).toBeDefined()
    expect(missingError).toBeDefined()
    expect(crossVendorError!.message).toBe(missingError!.message)
    expect(crossVendorError!.code).toBe(missingError!.code)
    expect(crossVendorError!.httpStatus).toBe(missingError!.httpStatus)
  })

  it('secaudit-reviews-immutable-vendor-on-resubmit — vendorId cannot change on upsert (C5)', async () => {
    const db = await freshDb()

    await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 4, body: 'first', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort },
    )

    await expect(
      submitReview(
        db,
        { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_B, rating: 4, body: 'swap vendor', purchaseId: PURCHASE_ID_1 },
        { verifiedPurchase: purchaserPort },
      ),
    ).rejects.toSatisfy((e) => isImmutableFieldError(e) && e.field === 'vendorId')

    const [row] = await db.select().from(review).where(eq(review.userId, USER_ID))
    expect(row?.vendorId).toBe(VENDOR_A)
  })

  it('secaudit-reviews-null-vendor-reply-denied — null-vendor reply throws byte-identical 404 (D6)', async () => {
    const db = await freshDb()

    const nullVendorReview = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: null, rating: 5, body: 'single-seller', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort },
    )

    const vendorAReview = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_B, vendorId: VENDOR_A, rating: 4, body: 'marketplace', purchaseId: PURCHASE_ID_2 },
      { verifiedPurchase: purchaserPort },
    )

    async function captureReplyError(
      reviewId: string,
      vendorId: string | null,
    ): Promise<ReviewNotFoundError> {
      try {
        await replyToReview(db, reviewId, vendorId as string, 'x')
        throw new Error('expected ReviewNotFoundError')
      } catch (e) {
        if (isReviewNotFoundError(e)) {
          return e
        }
        throw e
      }
    }

    const nullCaller = await captureReplyError(nullVendorReview.id, null)
    const emptyCaller = await captureReplyError(nullVendorReview.id, '')
    const missing = await captureReplyError(MISSING_REVIEW_ID, VENDOR_B)
    const wrongVendor = await captureReplyError(vendorAReview.id, VENDOR_B)

    for (const err of [nullCaller, emptyCaller, missing, wrongVendor]) {
      expect(isReviewNotFoundError(err)).toBe(true)
      expect(err.message).toBe(nullCaller.message)
      expect(err.code).toBe(nullCaller.code)
    }

    const [row] = await db.select().from(review).where(eq(review.id, nullVendorReview.id))
    expect(row?.vendorReply).toBeNull()
  })

  it('secaudit-reviews-body-length-db-check — DB CHECK rejects over-length body bypassing submit (defense-in-depth)', async () => {
    const db = await freshDb()
    const now = new Date()

    await expect(
      db.insert(review).values({
        productId: 'p',
        userId: 'u',
        purchaseId: 'purch-0000-0000-0000-000000000001',
        vendorId: 'v',
        rating: 5,
        body: 'x'.repeat(5001),
        status: 'approved',
        createdAt: now,
        updatedAt: now,
      }),
    ).rejects.toThrow()

    await db.insert(review).values({
      productId: 'p',
      userId: 'u',
      purchaseId: 'purch-0000-0000-0000-000000000001',
      vendorId: 'v',
      rating: 5,
      body: 'x'.repeat(5000),
      status: 'approved',
      createdAt: now,
      updatedAt: now,
    })
    expect(await rowCount(db)).toBe(1)

    await db.insert(review).values({
      productId: 'p2',
      userId: 'u2',
      purchaseId: 'purch-0000-0000-0000-000000000002',
      vendorId: 'v',
      rating: 5,
      body: null,
      status: 'approved',
      createdAt: now,
      updatedAt: now,
    })
    expect(await rowCount(db)).toBe(2)
  })

  it('reply-success-persists-vendorReply — vendor reply updates row and advances updatedAt', async () => {
    const db = await freshDb()

    const created = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: 'vA', rating: 4, body: 'great product', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort },
    )

    const [before] = await db.select().from(review).where(eq(review.id, created.id))
    expect(before?.vendorReply).toBeNull()

    await replyToReview(db, created.id, 'vA', 'thanks for the feedback')

    const [after] = await db.select().from(review).where(eq(review.id, created.id))
    expect(after?.vendorReply).toBe('thanks for the feedback')
    expect(after?.updatedAt.getTime()).toBeGreaterThan(before!.updatedAt.getTime())
  })

  it('secaudit-reviews-reply-length-bounded — vendorReply capped at boundary + DB CHECK (D10)', async () => {
    const db = await freshDb()

    const created = await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 5, body: 'seed', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort, moderation: noOpAutoApproveModerationAdapter },
    )

    await expect(
      replyToReview(db, created.id, VENDOR_A, 'x'.repeat(5001)),
    ).rejects.toSatisfy((e) => isReviewValidationError(e))

    const [afterReject] = await db.select().from(review).where(eq(review.id, created.id))
    expect(afterReject?.vendorReply).toBeNull()

    await replyToReview(db, created.id, VENDOR_A, 'x'.repeat(5000))

    const [afterAccept] = await db.select().from(review).where(eq(review.id, created.id))
    expect(afterAccept?.vendorReply).toHaveLength(5000)

    await expect(
      db
        .update(review)
        .set({ vendorReply: 'x'.repeat(5001), updatedAt: new Date() })
        .where(eq(review.id, created.id)),
    ).rejects.toThrow()
  })

  it('secaudit-reviews-body-length-bounded — body capped before port and moderation (D7)', async () => {
    const db = await freshDb()
    let scoreCalls = 0
    const countingModeration = {
      score: async () => {
        scoreCalls += 1
        return { action: 'approve' as const, score: 1 }
      },
    }

    await expect(
      submitReview(
        db,
        {
          productId: PRODUCT_ID,
          userId: USER_ID,
          vendorId: VENDOR_A,
          rating: 5,
          body: 'x'.repeat(5001),
          purchaseId: PURCHASE_ID_1,
        },
        { verifiedPurchase: purchaserPort, moderation: countingModeration },
      ),
    ).rejects.toSatisfy((e) => isReviewValidationError(e) && e.field === 'body')

    expect(await rowCount(db)).toBe(0)
    expect(scoreCalls).toBe(0)

    await submitReview(
      db,
      {
        productId: PRODUCT_ID,
        userId: USER_ID,
        vendorId: VENDOR_A,
        rating: 5,
        body: 'x'.repeat(5000),
        purchaseId: PURCHASE_ID_1,
      },
      { verifiedPurchase: purchaserPort },
    )

    expect(await rowCount(db)).toBe(1)
  })

  it('secaudit-reviews-malformed-reviewid-typed-error — non-uuid reviewId throws ReviewValidationError, never a raw DB throw (D9)', async () => {
    const db = await freshDb()

    // Seed one approved review so a raw uuid-parse error (if it leaked) would
    // be distinguishable from an empty table — the guard must fire regardless.
    await submitReview(
      db,
      { productId: PRODUCT_ID, userId: USER_ID, vendorId: VENDOR_A, rating: 4, body: 'seed', purchaseId: PURCHASE_ID_1 },
      { verifiedPurchase: purchaserPort, moderation: noOpAutoApproveModerationAdapter },
    )
    const before = await rowCount(db)

    const malformed = ['garbage', '', ' ', "1' OR '1'='1", 'x'.repeat(100_000), 'not-a-uuid-at-all']

    for (const bad of malformed) {
      // reply path — guard runs before the ownership SELECT
      await expect(replyToReview(db, bad, VENDOR_A, 'x')).rejects.toSatisfy(
        (e) => isReviewValidationError(e),
      )
      // moderate path — guard runs before the UPDATE
      await expect(moderateReview(db, bad, 'approve')).rejects.toSatisfy(
        (e) => isReviewValidationError(e),
      )
    }

    // No malformed id mutated or removed any row.
    expect(await rowCount(db)).toBe(before)

    // Cross-check the no-oracle split: a malformed id is a 400 (syntax),
    // distinct from the 404 a well-formed-but-unknown id returns — this split
    // leaks no existence/ownership info (it rejects on shape, before any lookup).
    let validationErr: { httpStatus?: number } | undefined
    try {
      await replyToReview(db, 'garbage', VENDOR_A, 'x')
    } catch (e) {
      if (isReviewValidationError(e)) validationErr = e
      else throw e
    }
    let notFoundErr: ReviewNotFoundError | undefined
    try {
      await replyToReview(db, MISSING_REVIEW_ID, VENDOR_A, 'x')
    } catch (e) {
      if (isReviewNotFoundError(e)) notFoundErr = e
      else throw e
    }
    expect(validationErr?.httpStatus).toBe(400)
    expect(notFoundErr?.httpStatus).toBe(404)
  })

  it('secaudit-reviews-cursor-malformed-typed-error — malformed cursor throws ReviewValidationError (D8)', async () => {
    const db = await freshDb()
    const crafted = [
      '|abc',
      'notadate|id',
      '0|',
      'x',
      '1970-01-01T00:00:00.000Z|' + 'z'.repeat(100_000),
    ]

    for (const cursor of crafted) {
      await expect(
        listReviews(db, PRODUCT_ID, { audience: 'public', cursor }),
      ).rejects.toSatisfy((e) => isReviewValidationError(e))
    }
  })
})
