// @vitest-environment node
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { sql } from 'drizzle-orm';
import { makeTestDb, closeTestDb, resetTables, type TestDb } from '../test/pglite-db.js';
import {
  createUser,
  createReferralLink,
  createReferral,
  seedReferralSettings,
} from '../test/fixtures.js';
import { createPgliteFraudHostStore } from '../test/host-store-fixture.js';

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

// Lazily import adapters after mock registration
const getDeviceFingerprint = () => import('./adapters/device-fingerprint.js');
const getIpCluster = () => import('./adapters/ip-cluster.js');
const getIdentityRing = () => import('./adapters/identity-ring.js');
const getVelocityConversion = () => import('./adapters/velocity-conversion.js');

/** Default enabled AdapterConfig */
const enabledCfg = { enabled: true };

// ─── Setup ────────────────────────────────────────────────────────────────────

describe('DB-touching fraud adapters integration', () => {
  beforeAll(async () => {
    testDb = await makeTestDb();
  });
  afterAll(async () => {
    await closeTestDb();
  });
  beforeEach(async () => {
    await resetTables(testDb);
    await seedReferralSettings(testDb);
  });

  // ─── device-fingerprint adapter ───────────────────────────────────────────────

  describe('deviceFingerprintAdapter (SIGNUP path)', () => {
    it('returns null for empty DB (no referrals with visitor_id_referrer)', async () => {
      const { deviceFingerprintAdapter } = await getDeviceFingerprint();
      const { id: userId } = await createUser(testDb);

      const signal = await deviceFingerprintAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: userId,
          refereeEmail: 'test@example.com',
          visitorId: 'visitor-abc',
        },
        enabledCfg,
      );

      expect(signal).toBeNull();
    });

    it('flags DEVICE_SEEN_AS_REFERRER when visitorId matches referrer in existing referral', async () => {
      const { deviceFingerprintAdapter } = await getDeviceFingerprint();
      const { id: referrerId } = await createUser(testDb);
      const { id: refereeId } = await createUser(testDb, { email: 'ref@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);

      // Insert a referral with visitor_id_referrer matching the suspicious visitorId
      const sharedVisitorId = 'shared-device-xyz';
      await testDb.execute(sql`
        UPDATE referrals
        SET visitor_id_referrer = ${sharedVisitorId}
        WHERE referrer_user_id = ${referrerId}
      `);
      // Actually insert the referral with that visitorId in referrer column
      const refId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, visitor_id_referrer)
        VALUES (${refId}, ${referrerId}, ${refereeId}, ${linkId}, 'referral', 'pending', ${sharedVisitorId})
      `);

      const { id: newRefereeId } = await createUser(testDb, { email: 'new@example.com' });

      const signal = await deviceFingerprintAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: newRefereeId,
          refereeEmail: 'new@example.com',
          visitorId: sharedVisitorId,
        },
        enabledCfg,
      );

      expect(signal).not.toBeNull();
      expect(signal!.adapter).toBe('device-fingerprint');
      expect(signal!.action).toBe('flag');
      expect(signal!.codes).toContain('DEVICE_SEEN_AS_REFERRER');
    });

    it('result shape has adapter, action, codes fields', async () => {
      const { deviceFingerprintAdapter } = await getDeviceFingerprint();
      const { id: referrerId } = await createUser(testDb);
      const { id: refereeId } = await createUser(testDb, { email: 'ref2@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);

      const visitorId = 'shape-test-visitor';
      const refId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, visitor_id_referrer)
        VALUES (${refId}, ${referrerId}, ${refereeId}, ${linkId}, 'referral', 'pending', ${visitorId})
      `);

      const { id: newUser } = await createUser(testDb, { email: 'shapetest@example.com' });
      const signal = await deviceFingerprintAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: newUser,
          refereeEmail: 'shapetest@example.com',
          visitorId,
        },
        enabledCfg,
      );

      expect(signal).not.toBeNull();
      expect(typeof signal!.adapter).toBe('string');
      expect(typeof signal!.action).toBe('string');
      expect(Array.isArray(signal!.codes)).toBe(true);
    });
  });

  // ─── ip-cluster adapter ───────────────────────────────────────────────────────

  describe('ipClusterAdapter (SIGNUP path)', () => {
    it('returns null for empty DB (no referrals sharing ip)', async () => {
      const { ipClusterAdapter } = await getIpCluster();
      const { id: userId } = await createUser(testDb);

      const signal = await ipClusterAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: userId,
          refereeEmail: 'ip@example.com',
          ipHash: 'hash-unique-000',
        },
        { enabled: true, params: { clusterSize: 3, windowHours: 4 } },
      );

      expect(signal).toBeNull();
    });

    it('flags IP_CLUSTER when enough referrals share same ip_hash_referee within window', async () => {
      const { ipClusterAdapter } = await getIpCluster();

      // Create 3 referrals sharing the same ip hash (threshold = 3)
      const sharedIp = 'shared-ip-hash-abc';
      for (let i = 0; i < 3; i++) {
        const { id: rrid } = await createUser(testDb, { email: `rr${i}@example.com` });
        const { id: reid } = await createUser(testDb, { email: `re${i}@example.com` });
        const { id: lid } = await createReferralLink(testDb, rrid);
        const refId = crypto.randomUUID();
        await testDb.execute(sql`
          INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, ip_hash_referee)
          VALUES (${refId}, ${rrid}, ${reid}, ${lid}, 'referral', 'pending', ${sharedIp})
        `);
      }

      const { id: newUser } = await createUser(testDb, { email: 'cluster@example.com' });

      const signal = await ipClusterAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: newUser,
          refereeEmail: 'cluster@example.com',
          ipHash: sharedIp,
        },
        { enabled: true, params: { clusterSize: 3, windowHours: 4 } },
      );

      expect(signal).not.toBeNull();
      expect(signal!.adapter).toBe('ip-cluster');
      expect(signal!.action).toBe('flag');
      expect(signal!.codes).toContain('IP_CLUSTER');
    });

    it('result shape has adapter, action, codes fields', async () => {
      const { ipClusterAdapter } = await getIpCluster();
      const sharedIp = 'ip-shape-test';
      for (let i = 0; i < 3; i++) {
        const { id: rrid } = await createUser(testDb, { email: `rrsh${i}@example.com` });
        const { id: reid } = await createUser(testDb, { email: `resh${i}@example.com` });
        const { id: lid } = await createReferralLink(testDb, rrid);
        const refId = crypto.randomUUID();
        await testDb.execute(sql`
          INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, ip_hash_referee)
          VALUES (${refId}, ${rrid}, ${reid}, ${lid}, 'referral', 'pending', ${sharedIp})
        `);
      }

      const { id: nu } = await createUser(testDb, { email: 'ipsh@example.com' });
      const signal = await ipClusterAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: nu,
          refereeEmail: 'ipsh@example.com',
          ipHash: sharedIp,
        },
        { enabled: true, params: { clusterSize: 3, windowHours: 4 } },
      );

      expect(signal).not.toBeNull();
      expect(typeof signal!.adapter).toBe('string');
      expect(typeof signal!.action).toBe('string');
      expect(Array.isArray(signal!.codes)).toBe(true);
    });
  });

  // ─── identity-ring adapter ────────────────────────────────────────────────────

  describe('identityRingAdapter (EARN path)', () => {
    it('returns null when referrer and referee are distinct users with no matching fields', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: referrerId } = await createUser(testDb);
      const { id: refereeId } = await createUser(testDb, { email: 'referee-ring@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const signal = await identityRingAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        enabledCfg,
      );

      expect(signal).toBeNull();
    });

    it('blocks SELF_REFERRAL_USER_ID when referrer === referee', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: userId } = await createUser(testDb);

      const signal = await identityRingAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId: crypto.randomUUID(),
          referrerUserId: userId,
          refereeUserId: userId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        enabledCfg,
      );

      expect(signal).not.toBeNull();
      expect(signal!.action).toBe('block');
      expect(signal!.codes).toContain('SELF_REFERRAL_USER_ID');
    });

    it('blocks SELF_REFERRAL_EMAIL_CANONICAL when users share canonical email index', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: referrerId } = await createUser(testDb, { email: 'canonical-rr@example.com' });
      const { id: refereeId } = await createUser(testDb, { email: 'canonical-re@example.com' });

      // Set matching canonical email index on both users
      const sharedCanonical = 'shared.canonical@example.com';
      await testDb.execute(sql`
        UPDATE users SET email_canonical_index = ${sharedCanonical} WHERE id = ${referrerId}
      `);
      await testDb.execute(sql`
        UPDATE users SET email_canonical_index = ${sharedCanonical} WHERE id = ${refereeId}
      `);

      const signal = await identityRingAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId: crypto.randomUUID(),
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        enabledCfg,
      );

      expect(signal).not.toBeNull();
      expect(signal!.action).toBe('block');
      expect(signal!.codes).toContain('SELF_REFERRAL_EMAIL_CANONICAL');
    });

    it('result shape has adapter, action, codes fields', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: userId } = await createUser(testDb, { email: 'ringshape@example.com' });

      const signal = await identityRingAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId: crypto.randomUUID(),
          referrerUserId: userId,
          refereeUserId: userId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 500,
        },
        enabledCfg,
      );

      expect(signal).not.toBeNull();
      expect(typeof signal!.adapter).toBe('string');
      expect(typeof signal!.action).toBe('string');
      expect(Array.isArray(signal!.codes)).toBe(true);
    });
  });

  // ─── velocity-conversion adapter ──────────────────────────────────────────────

  describe('velocityConversionAdapter (EARN path)', () => {
    it('returns null for empty DB (referral + purchase not found)', async () => {
      const { velocityConversionAdapter } = await getVelocityConversion();
      const { id: referrerId } = await createUser(testDb);
      const { id: refereeId } = await createUser(testDb, { email: 'vel@example.com' });

      const signal = await velocityConversionAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId: crypto.randomUUID(),
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        enabledCfg,
      );

      // No referral/purchase rows → no time-to-convert check → may still return null
      // (conversion-rate path also needs clicks > 0)
      expect(signal).toBeNull();
    });

    it('flags FAST_CONVERSION when purchase happens within minMinutes of referral', async () => {
      const { velocityConversionAdapter } = await getVelocityConversion();
      const { id: referrerId } = await createUser(testDb);
      const { id: refereeId } = await createUser(testDb, { email: 'fastconv@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);

      const now = new Date();
      const oneMinuteAgo = new Date(now.getTime() - 60_000);

      // Insert referral created 1 minute ago
      const referralId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, created_at)
        VALUES (${referralId}, ${referrerId}, ${refereeId}, ${linkId}, 'referral', 'pending', ${oneMinuteAgo.toISOString()})
      `);

      // Insert purchase + deal (vendors table is not in pglite fraud DDL — omit vendor_id)
      const dealId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO deals (id, deal_type) VALUES (${dealId}, 'COUPON')
      `);

      const purchaseId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO purchases (id, user_id, deal_id, created_at)
        VALUES (${purchaseId}, ${refereeId}, ${dealId}, ${now.toISOString()})
      `);

      const signal = await velocityConversionAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId,
          platformNetAgorot: 1000,
        },
        { enabled: true, params: { minMinutesToConvert: 5 } },
      );

      expect(signal).not.toBeNull();
      expect(signal!.adapter).toBe('velocity-conversion');
      expect(signal!.action).toBe('flag');
      expect(signal!.codes).toContain('FAST_CONVERSION');
    });

    it('result shape has adapter, action, codes fields when signal fires', async () => {
      const { velocityConversionAdapter } = await getVelocityConversion();
      const { id: referrerId } = await createUser(testDb, { email: 'vcshape-rr@example.com' });
      const { id: refereeId } = await createUser(testDb, { email: 'vcshape-re@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);

      const now = new Date();
      const twoMinsAgo = new Date(now.getTime() - 2 * 60_000);

      const referralId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO referrals (id, referrer_user_id, referee_user_id, link_id, kind, status, created_at)
        VALUES (${referralId}, ${referrerId}, ${refereeId}, ${linkId}, 'referral', 'pending', ${twoMinsAgo.toISOString()})
      `);

      const dealId = crypto.randomUUID();
      await testDb.execute(sql`INSERT INTO deals (id, deal_type) VALUES (${dealId}, 'COUPON')`);

      const purchaseId = crypto.randomUUID();
      await testDb.execute(sql`
        INSERT INTO purchases (id, user_id, deal_id, created_at)
        VALUES (${purchaseId}, ${refereeId}, ${dealId}, ${now.toISOString()})
      `);

      const signal = await velocityConversionAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId,
          platformNetAgorot: 500,
        },
        { enabled: true, params: { minMinutesToConvert: 5 } },
      );

      expect(signal).not.toBeNull();
      expect(typeof signal!.adapter).toBe('string');
      expect(typeof signal!.action).toBe('string');
      expect(Array.isArray(signal!.codes)).toBe(true);
    });
  });
});
