// @vitest-environment node
/**
 * EARN handler integration oracle — fraud gate fail-closed → computeCommission → accrueCommission.
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { sql, eq } from 'drizzle-orm'
import { ledgerEntries } from '@platform-modules/ledger'
import { getVesting, ledgerEntryVesting } from '@platform-modules/ledger/vesting'
import { makeTestDb, closeTestDb, resetTables, type TestDb } from './test/pglite-db.js'
import {
  createUser,
  createReferralLink,
  seedReferralSettings,
} from './test/fixtures.js'
import { createPgliteFraudHostStore } from './test/host-store-fixture.js'
import { affiliateEnrollmentsTable, affiliateEntriesTable, referralsTable } from './schema.js'
import type { FraudAdapter } from './fraud/types.js'
import type { CommissionPolicy } from './commission.js'
import { computeAffiliateCommission, resolveAffiliateTierPct } from './commission.js'
import type { ReferralSettings } from './settings.js'
import type { Transaction } from '@platform-modules/db'
import type { HarnessSchema } from './test/harness-schema.js'

let testDb: TestDb
const testHost = () => createPgliteFraudHostStore(testDb)

const getEarn = () => import('./earn.js')
const getRegistry = () => import('./fraud/registry.js')

let _mockKeySeq = 0
function nextKey() {
  return `earn-mock-${++_mockKeySeq}`
}

const EARN_TEST_REFERRAL_SETTINGS: ReferralSettings = {
  affiliatePct: 30,
  tier1Pct: 3,
  tier2Pct: 4,
  tier3Pct: 5,
  tier2MinSales: 20,
  tier3MinSales: 50,
  referralPct: 2,
  affiliateWindowDays: 30,
  affiliateMaxOrders: 30,
  cookieDays: 30,
  rewardAgorot: 2000,
  refereeDiscountAgorot: 2000,
  holdDays: 30,
  withdrawalMinAgorot: 30000,
  autoApproveLifetimeAgorot: 50000,
  brandKeywordBlocklist: ['acme', 'acme-shop'],
  disallowedRefererHosts: [],
  tosVersion: 'v1-2026-05-28',
  disputeWindowDays: 120,
  reservePct: 0,
  reserveReleaseDays: 120,
  fraudConfig: {},
}

const earnTestPolicy: CommissionPolicy = {
  resolve({ amountPaidMinor, context }) {
    const pct = resolveAffiliateTierPct(
      context.rollingSalesCount ?? 0,
      EARN_TEST_REFERRAL_SETTINGS,
    )
    return computeAffiliateCommission(amountPaidMinor, pct)
  },
}

function earnTx(): Transaction<HarnessSchema> {
  return testDb as unknown as Transaction<HarnessSchema>
}

function purchaseIdempotencyKey(purchaseId: string) {
  return `affiliate_commission:purchase:${purchaseId}`
}

async function seedQualifiedAffiliateEarn(
  db: TestDb,
): Promise<{ referrerId: string; refereeId: string; referralId: string; purchaseId: string }> {
  const { id: referrerId } = await createUser(db, { email: `rr-${crypto.randomUUID()}@test.invalid` })
  const { id: refereeId } = await createUser(db, { email: `re-${crypto.randomUUID()}@test.invalid` })
  const { id: linkId } = await createReferralLink(db, referrerId, { kind: 'affiliate' })
  const referralId = crypto.randomUUID()
  const purchaseId = crypto.randomUUID()
  const enrollmentId = crypto.randomUUID()

  await db.execute(sql`
    INSERT INTO referrals (
      id, referrer_user_id, referee_user_id, link_id, kind, status, qualified_at,
      commission_window_ends_at, commission_orders_remaining
    ) VALUES (
      ${referralId}, ${referrerId}, ${refereeId}, ${linkId}, 'affiliate', 'qualified', NOW(),
      NOW() + INTERVAL '30 days', 10
    )
  `)

  await db.insert(affiliateEnrollmentsTable).values({
    id: enrollmentId,
    userId: referrerId,
    status: 'active',
    tosAcceptedAt: new Date(),
    tosVersion: 'test',
  })

  return { referrerId, refereeId, referralId, purchaseId }
}

describe('processReferralEarn integration', () => {
  beforeAll(async () => {
    testDb = await makeTestDb()
  })

  afterAll(async () => {
    await closeTestDb()
  })

  beforeEach(async () => {
    await resetTables(testDb)
    await seedReferralSettings(testDb)
    const { resetAdaptersForTest } = await getRegistry()
    resetAdaptersForTest()
  })

  it('pass → computeCommission → accrueCommission with pending state', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return null
      },
    } as FraudAdapter<unknown>)

    const { referrerId, refereeId, referralId, purchaseId } =
      await seedQualifiedAffiliateEarn(testDb)
    const matureAt = new Date(Date.now() + 30 * 86_400_000)

    const result = await processReferralEarn(earnTx(), {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt,
    })

    expect(result.earned).toBe(true)
    if (!result.earned) return
    expect(result.commissionMinor).toBe(300n)
    expect(result.inserted).toBe(true)
    expect(result.state).toBe('pending')

    const ledgerRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(ledgerRows).toHaveLength(1)
    expect(ledgerRows[0]!.delta).toBe(300n)

    const sideRows = await testDb
      .select()
      .from(affiliateEntriesTable)
      .where(eq(affiliateEntriesTable.sourceId, purchaseId))
    expect(sideRows).toHaveLength(1)
    expect(sideRows[0]!.entryType).toBe('affiliate_commission')
    expect(sideRows[0]!.sourceType).toBe('purchase')
    expect(sideRows[0]!.sourceId).toBe(purchaseId)
    expect(sideRows[0]!.referralId).toBe(referralId)
    expect(sideRows[0]!.entryId).toBe(ledgerRows[0]!.id)

    const vestingRows = await testDb
      .select()
      .from(ledgerEntryVesting)
      .where(eq(ledgerEntryVesting.entryId, sideRows[0]!.entryId))
    expect(vestingRows).toHaveLength(1)
    expect(vestingRows[0]!.sweptAt).toBeNull()
    expect(vestingRows[0]!.matureAt).not.toBeNull()
    expect(vestingRows[0]!.matureAt!.getTime()).toBe(matureAt.getTime())

    const vesting = await getVesting(testDb, referrerId)
    expect(vesting.pendingMinor).toBe(300n)
    expect(vesting.maturedMinor).toBe(0n)
    expect(vesting.lifetimeEarnedMinor).toBe(300n)
  })

  it('idempotent re-delivery does not double-credit', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return null
      },
    } as FraudAdapter<unknown>)

    const { referrerId, refereeId, purchaseId } = await seedQualifiedAffiliateEarn(testDb)
    const matureAt = new Date(Date.now() + 30 * 86_400_000)
    const input = {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt,
    }

    await processReferralEarn(earnTx(), input)
    const second = await processReferralEarn(earnTx(), input)

    expect(second.earned).toBe(true)
    if (!second.earned) return
    expect(second.inserted).toBe(false)

    const ledgerRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(ledgerRows).toHaveLength(1)

    const sideRows = await testDb
      .select()
      .from(affiliateEntriesTable)
      .where(eq(affiliateEntriesTable.sourceId, purchaseId))
    expect(sideRows).toHaveLength(1)

    const vesting = await getVesting(testDb, referrerId)
    expect(vesting.pendingMinor).toBe(300n)
  })

  it('fraud HOLD blocks accrual and quarantines referral', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return { adapter: key, action: 'hold', codes: ['test_hold'] }
      },
    } as FraudAdapter<unknown>)

    const { refereeId, referralId, purchaseId } = await seedQualifiedAffiliateEarn(testDb)

    const result = await processReferralEarn(earnTx(), {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt: new Date(Date.now() + 30 * 86_400_000),
    })

    expect(result.earned).toBe(false)
    if (result.earned) return
    expect(result.reason).toBe('FRAUD_HOLD')

    const ledgerRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(ledgerRows).toHaveLength(0)

    const sideRows = await testDb
      .select()
      .from(affiliateEntriesTable)
      .where(eq(affiliateEntriesTable.sourceId, purchaseId))
    expect(sideRows).toHaveLength(0)

    const [ref] = await testDb
      .select()
      .from(referralsTable)
      .where(eq(referralsTable.id, referralId))
    expect(ref!.quarantinedAt).not.toBeNull()
  })

  it('EARN fail-closed: adapter throw yields hold and blocks accrual', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        throw new Error('simulated adapter failure')
      },
    } as FraudAdapter<unknown>)

    const { refereeId, purchaseId } = await seedQualifiedAffiliateEarn(testDb)

    const result = await processReferralEarn(earnTx(), {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt: new Date(Date.now() + 30 * 86_400_000),
    })

    expect(result.earned).toBe(false)
    if (result.earned) return
    expect(result.reason).toBe('FRAUD_HOLD')

    const ledgerRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(ledgerRows).toHaveLength(0)

    const sideRows = await testDb
      .select()
      .from(affiliateEntriesTable)
      .where(eq(affiliateEntriesTable.sourceId, purchaseId))
    expect(sideRows).toHaveLength(0)
  })

  it('fraud BLOCK writes zero audit row and quarantines without positive accrual', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return { adapter: key, action: 'block', codes: ['test_block'] }
      },
    } as FraudAdapter<unknown>)

    const { referrerId, refereeId, referralId, purchaseId } =
      await seedQualifiedAffiliateEarn(testDb)

    const result = await processReferralEarn(earnTx(), {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt: new Date(Date.now() + 30 * 86_400_000),
    })

    expect(result.earned).toBe(false)
    if (result.earned) return
    expect(result.reason).toBe('FRAUD_BLOCK')

    const ledgerRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(
        eq(ledgerEntries.idempotencyKey, `affiliate_commission:fraud_block:${purchaseId}`),
      )
    expect(ledgerRows).toHaveLength(1)
    expect(ledgerRows[0]!.delta).toBe(0n)

    const canonicalRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(canonicalRows).toHaveLength(0)

    const sideRows = await testDb
      .select()
      .from(affiliateEntriesTable)
      .where(eq(affiliateEntriesTable.sourceId, purchaseId))
    expect(sideRows).toHaveLength(1)
    expect(sideRows[0]!.memo).toBe('fraud:block')
    expect(sideRows[0]!.entryType).toBe('affiliate_commission')
    expect(sideRows[0]!.sourceType).toBe('fraud_block')
    expect(sideRows[0]!.sourceId).toBe(purchaseId)
    expect(sideRows[0]!.referralId).toBe(referralId)
    expect(sideRows[0]!.entryId).toBe(ledgerRows[0]!.id)

    const vestingRows = await testDb
      .select()
      .from(ledgerEntryVesting)
      .where(eq(ledgerEntryVesting.entryId, sideRows[0]!.entryId))
    expect(vestingRows).toHaveLength(0)

    const vesting = await getVesting(testDb, referrerId)
    expect(vesting.pendingMinor).toBe(0n)
    expect(vesting.maturedMinor).toBe(0n)
    expect(vesting.lifetimeEarnedMinor).toBe(0n)

    const [ref] = await testDb
      .select()
      .from(referralsTable)
      .where(eq(referralsTable.id, referralId))
    expect(ref!.quarantinedAt).not.toBeNull()
  })

  it('a cleared false-positive BLOCK re-earns — the audit row must not tombstone the canonical key', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    let verdict: 'block' | 'allow' = 'block'
    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return verdict === 'block'
          ? { adapter: key, action: 'block', codes: ['test_block'] }
          : { adapter: key, action: 'allow', codes: [] }
      },
    } as FraudAdapter<unknown>)

    const { referrerId, refereeId, referralId, purchaseId } =
      await seedQualifiedAffiliateEarn(testDb)

    const earnInput = {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId,
      buyerUserId: refereeId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt: new Date(Date.now() + 30 * 86_400_000),
    }

    const blocked = await processReferralEarn(earnTx(), earnInput)
    expect(blocked.earned).toBe(false)

    // Admin clears the false positive; the adapter now allows.
    await testDb
      .update(referralsTable)
      .set({ quarantinedAt: null })
      .where(eq(referralsTable.id, referralId))
    verdict = 'allow'

    const reEarn = await processReferralEarn(earnTx(), earnInput)
    expect(reEarn.earned).toBe(true)
    if (!reEarn.earned) return
    expect(reEarn.inserted).toBe(true)
    expect(reEarn.commissionMinor > 0n).toBe(true)

    const canonicalRows = await testDb
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, purchaseIdempotencyKey(purchaseId)))
    expect(canonicalRows).toHaveLength(1)
    expect(canonicalRows[0]!.delta).toBe(reEarn.commissionMinor)

    const vesting = await getVesting(testDb, referrerId)
    expect(vesting.pendingMinor).toBe(reEarn.commissionMinor)
  })

  it('returns NO_REFERRAL when buyer has no referral row', async () => {
    const { processReferralEarn } = await getEarn()
    const { registerAdapter } = await getRegistry()
    const key = nextKey()

    registerAdapter({
      key,
      points: ['EARN'],
      async evaluate() {
        return null
      },
    } as FraudAdapter<unknown>)

    const { id: buyerId } = await createUser(testDb)

    const result = await processReferralEarn(earnTx(), {
      host: testHost(),
      policy: earnTestPolicy,
      fraudConfig: { [key]: { enabled: true } },
      purchaseId: crypto.randomUUID(),
      buyerUserId: buyerId,
      amountPaidMinor: 10_000n,
      platformNetMinor: 9_000n,
      matureAt: new Date(),
    })

    expect(result).toEqual({ earned: false, reason: 'NO_REFERRAL' })
  })
})
