// @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 type { FraudAdapter } from './types.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);

// Lazy imports
const getRegistry = () => import('./registry.js');
const getEmailCanonical = () => import('./adapters/email-canonical.js');
const getIdentityRing = () => import('./adapters/identity-ring.js');

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Unique adapter key per test invocation to avoid cross-test contamination. */
let _mockKeySeq = 0;
function nextKey() {
  return `mock-adapter-${++_mockKeySeq}`;
}

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

describe('fraud pipeline — decision points', () => {
  beforeAll(async () => {
    testDb = await makeTestDb();
  });
  afterAll(async () => {
    await closeTestDb();
  });
  beforeEach(async () => {
    await resetTables(testDb);
    await seedReferralSettings(testDb);
    const { resetAdaptersForTest } = await getRegistry();
    resetAdaptersForTest();
  });

  // ─── 1. Decision-point shape contract ─────────────────────────────────────

  describe('PipelineResult shape', () => {
    it('result has action, signals fields — pass when no adapters fire', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      // Register a pass-only adapter so we get a deterministic, isolated result
      const key = nextKey();
      registerAdapter({
        key,
        points: ['EARN'],
        async evaluate() {
          return null; // always pass
        },
      } as FraudAdapter<unknown>);

      const { id: referrerId } = await createUser(testDb, { email: `shape-rr-${key}@example.com` });
      const { id: refereeId } = await createUser(testDb, { email: `shape-re-${key}@example.com` });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result).toHaveProperty('action');
      expect(result).toHaveProperty('signals');
      expect(Array.isArray(result.signals)).toBe(true);
      expect(result.action).toBe('pass');
    });
  });

  // ─── 2. CLICK decision point: empty DB → pass ─────────────────────────────

  describe('CLICK decision point', () => {
    it('empty DB → action = pass (no adapters registered for CLICK in test scope)', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      // Register an always-pass adapter for CLICK
      const key = nextKey();
      registerAdapter({
        key,
        points: ['CLICK'],
        async evaluate() {
          return null;
        },
      } as FraudAdapter<unknown>);

      const result = await runFraudPipeline({
        point: 'CLICK',
        ctx: {
          ipHash: 'hash-abc',
          userAgent: 'Mozilla/5.0',
          country: 'IL',
          refererUrl: 'https://example.com',
          linkId: crypto.randomUUID(),
        },
        userId: crypto.randomUUID(),
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('pass');
      expect(result.signals).toHaveLength(0);
    });
  });

  // ─── 3. SIGNUP decision point: empty DB → pass ────────────────────────────

  describe('SIGNUP decision point', () => {
    it('empty DB → action = pass (email-canonical adapter, no existing user)', async () => {
      const { emailCanonicalAdapter } = await getEmailCanonical();
      const { id: refereeId } = await createUser(testDb, { email: 'signup-clean@example.com' });

      const result = await emailCanonicalAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: refereeId,
          refereeEmail: 'signup-clean@example.com',
        },
        { enabled: true },
      );

      expect(result).toBeNull();
    });

    it('SIGNUP → block when canonical email collision (same gmail+alias)', async () => {
      const { emailCanonicalAdapter } = await getEmailCanonical();

      // Existing user with canonical email j.o.h.n@gmail.com → john@gmail.com
      await testDb.execute(sql`
        INSERT INTO users (id, email, email_index, email_canonical_index, account_state)
        VALUES (
          ${crypto.randomUUID()},
          ${'j.o.h.n@gmail.com'},
          ${'j.o.h.n@gmail.com'},
          ${'john@gmail.com'},
          'ACTIVE'
        )
      `);

      const { id: newRefereeId } = await createUser(testDb, { email: 'john+spam@gmail.com' });

      const signal = await emailCanonicalAdapter.evaluate(
        {
          db: testDb,
          host: testHost(),
          refereeUserId: newRefereeId,
          // john+spam@gmail.com canonicalizes → john@gmail.com = collision
          refereeEmail: 'john+spam@gmail.com',
        },
        { enabled: true },
      );

      expect(signal).not.toBeNull();
      expect(signal!.adapter).toBe('email-canonical');
      expect(signal!.action).toBe('block');
      expect(signal!.codes).toContain('CANONICAL_EMAIL_EXISTS');
    });
  });

  // ─── 4. EARN decision point: empty DB → pass ──────────────────────────────

  describe('EARN decision point', () => {
    it('empty DB → pass (identity-ring, distinct users, no shared phone/canonical)', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: referrerId } = await createUser(testDb, { email: 'earn-rr@example.com' });
      const { id: refereeId } = await createUser(testDb, { email: 'earn-re@example.com' });

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

      expect(result).toBeNull();
    });

    it('EARN → block when referrer === referee (self-referral)', async () => {
      const { identityRingAdapter } = await getIdentityRing();
      const { id: userId } = await createUser(testDb, { email: 'self-ref@example.com' });

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

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

    it('EARN → block when referrer and referee share canonical email', async () => {
      const { identityRingAdapter } = await getIdentityRing();

      const referrerId = crypto.randomUUID();
      const refereeId = crypto.randomUUID();
      const sharedCanonical = 'johnshared@gmail.com';

      await testDb.execute(sql`
        INSERT INTO users (id, email, email_index, email_canonical_index, account_state)
        VALUES (${referrerId}, ${'rr-canon@example.com'}, ${'rr-canon@example.com'}, ${sharedCanonical}, 'ACTIVE')
      `);
      await testDb.execute(sql`
        INSERT INTO users (id, email, email_index, email_canonical_index, account_state)
        VALUES (${refereeId}, ${'re-canon@example.com'}, ${'re-canon@example.com'}, ${sharedCanonical}, 'ACTIVE')
      `);

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

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

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

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

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

  // ─── 5. WITHDRAW decision point: empty DB → pass ──────────────────────────

  describe('WITHDRAW decision point', () => {
    it('empty DB → action = pass (no adapters fire)', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['WITHDRAW'],
        async evaluate() {
          return null;
        },
      } as FraudAdapter<unknown>);

      const { id: userId } = await createUser(testDb, { email: `withdraw-${key}@example.com` });

      const result = await runFraudPipeline({
        point: 'WITHDRAW',
        ctx: {
          db: testDb,
          host: testHost(),
          userId,
          amountAgorot: 30000,
          affiliateId: crypto.randomUUID(),
        },
        userId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('pass');
      expect(result.signals).toHaveLength(0);
    });
  });

  // ─── 6. Pipeline with mock adapter that always flags ──────────────────────

  describe('pipeline with registered mock adapter', () => {
    it('mock adapter returning flag → pipeline resolves action = flag', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['EARN'],
        async evaluate() {
          return {
            adapter: key,
            action: 'flag' as const,
            codes: ['MOCK_FLAG'],
          };
        },
      } as FraudAdapter<unknown>);

      const { id: referrerId } = await createUser(testDb, {
        email: `mock-flag-rr-${key}@example.com`,
      });
      const { id: refereeId } = await createUser(testDb, {
        email: `mock-flag-re-${key}@example.com`,
      });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('flag');
      expect(result.signals.some((s) => s.codes.includes('MOCK_FLAG'))).toBe(true);
    });

    it('mock adapter returning block → pipeline resolves action = block', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['SIGNUP'],
        async evaluate() {
          return {
            adapter: key,
            action: 'block' as const,
            codes: ['MOCK_BLOCK'],
          };
        },
      } as FraudAdapter<unknown>);

      const { id: userId } = await createUser(testDb, {
        email: `mock-block-${key}@example.com`,
      });

      const result = await runFraudPipeline({
        point: 'SIGNUP',
        ctx: {
          db: testDb,
          host: testHost(),
          refereeUserId: userId,
          refereeEmail: `mock-block-${key}@example.com`,
        },
        userId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('block');
      expect(result.signals).toHaveLength(1);
      expect(result.signals[0]!.codes).toContain('MOCK_BLOCK');
    });

    it('disabled adapter config → adapter skipped, result = pass', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

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

      const { id: referrerId } = await createUser(testDb, {
        email: `disabled-rr-${key}@example.com`,
      });
      const { id: refereeId } = await createUser(testDb, {
        email: `disabled-re-${key}@example.com`,
      });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: false } }, // disabled
        db: testDb,
        persistEvents: false,
      });

      // Only this adapter is checked; others may have fired (from registry state)
      // We verify the mock did not fire by checking no signal with our key
      const ourSignal = result.signals.find((s) => s.adapter === key);
      expect(ourSignal).toBeUndefined();
    });
  });

  // ─── 7. fraud_events written to DB when pipeline flags/blocks ─────────────

  describe('fraud_events persistence', () => {
    it('fraud_events row inserted when mock adapter flags and persistEvents = true', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['EARN'],
        async evaluate() {
          return {
            adapter: key,
            action: 'flag' as const,
            codes: ['PERSIST_TEST'],
          };
        },
      } as FraudAdapter<unknown>);

      const { id: referrerId } = await createUser(testDb, {
        email: `persist-rr-${key}@example.com`,
      });
      const { id: refereeId } = await createUser(testDb, {
        email: `persist-re-${key}@example.com`,
      });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: true, // explicitly enable persistence
      });

      const rows = (await testDb.execute(sql`
        SELECT id, user_id, decision_point, adapter, action, codes
        FROM fraud_events
        WHERE adapter = ${key}
      `)) as { rows: Array<Record<string, unknown>> }

      expect(rows.rows.length).toBeGreaterThanOrEqual(1);
      const row = rows.rows[0] as {
        user_id: string;
        decision_point: string;
        adapter: string;
        action: string;
        codes: string[];
      };
      expect(row.user_id).toBe(referrerId);
      // decision_point persists lowercase by contract (toStoredDecisionPoint: EARN→earn);
      // the raw DB row is the storage layer, not the uppercase in-memory DecisionPoint enum.
      expect(row.decision_point).toBe('earn');
      expect(row.adapter).toBe(key);
      expect(row.action).toBe('flag');
    });

    it('fraud_events NOT written when persistEvents = false', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['EARN'],
        async evaluate() {
          return {
            adapter: key,
            action: 'flag' as const,
            codes: ['NO_PERSIST_TEST'],
          };
        },
      } as FraudAdapter<unknown>);

      const { id: referrerId } = await createUser(testDb, {
        email: `nopersist-rr-${key}@example.com`,
      });
      const { id: refereeId } = await createUser(testDb, {
        email: `nopersist-re-${key}@example.com`,
      });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false, // skip persistence
      });

      const rows = (await testDb.execute(sql`
        SELECT id FROM fraud_events WHERE adapter = ${key}
      `)) as { rows: unknown[] }

      expect(rows.rows.length).toBe(0);
    });

    it('fraud_events row includes referralId when provided', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

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

      const { id: referrerId } = await createUser(testDb, {
        email: `refid-rr-${key}@example.com`,
      });
      const { id: refereeId } = await createUser(testDb, {
        email: `refid-re-${key}@example.com`,
      });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: true,
      });

      const rows = (await testDb.execute(sql`
        SELECT referral_id FROM fraud_events WHERE adapter = ${key}
      `)) as { rows: Array<{ referral_id: string }> }

      expect(rows.rows.length).toBeGreaterThanOrEqual(1);
      const row = rows.rows[0] as { referral_id: string };
      expect(row.referral_id).toBe(referralId);
    });
  });


  // ─── EARN fail-closed (adapter throw → hold, never silent allow) ───────────

  describe('EARN fail-closed', () => {
    it('zero registered EARN adapters yields hold — never silent pass', async () => {
      const { runFraudPipeline } = await getRegistry();

      const { id: referrerId } = await createUser(testDb, { email: 'earn-empty-adapters-rr@example.com' });
      const { id: refereeId } = await createUser(testDb, { email: 'earn-empty-adapters-re@example.com' });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: {},
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('hold');
      expect(result.signals).toHaveLength(0);
    });

    it('EARN holds when all registered adapters are config-disabled', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

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

      const { id: referrerId } = await createUser(testDb, { email: `earn-all-disabled-rr-${key}@example.com` });
      const { id: refereeId } = await createUser(testDb, { email: `earn-all-disabled-re-${key}@example.com` });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: false } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('hold');
      expect(result.signals).toHaveLength(0);
    });

    it('adapter throw on EARN yields hold with adapter_error, never pass', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

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

      const { id: referrerId } = await createUser(testDb, { email: `failclosed-rr-${key}@example.com` });
      const { id: refereeId } = await createUser(testDb, { email: `failclosed-re-${key}@example.com` });
      const { id: linkId } = await createReferralLink(testDb, referrerId);
      const { id: referralId } = await createReferral(testDb, {
        referrerUserId: referrerId,
        refereeUserId: refereeId,
        linkId,
      });

      const result = await runFraudPipeline({
        point: 'EARN',
        ctx: {
          db: testDb,
          host: testHost(),
          referralId,
          referrerUserId: referrerId,
          refereeUserId: refereeId,
          purchaseId: crypto.randomUUID(),
          platformNetAgorot: 1000,
        },
        userId: referrerId,
        referralId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('hold');
      expect(result.signals.some((s) => s.codes.includes('adapter_error'))).toBe(true);
      expect(result.action).not.toBe('pass');
    });

    it('adapter throw on SIGNUP is swallowed (not fail-closed)', async () => {
      const { runFraudPipeline, registerAdapter } = await getRegistry();

      const key = nextKey();
      registerAdapter({
        key,
        points: ['SIGNUP'],
        async evaluate() {
          throw new Error('signup adapter failure');
        },
      } as FraudAdapter<unknown>);

      const { id: userId } = await createUser(testDb, { email: `signup-throw-${key}@example.com` });

      const result = await runFraudPipeline({
        point: 'SIGNUP',
        ctx: {
          db: testDb,
          host: testHost(),
          refereeUserId: userId,
          refereeEmail: `signup-throw-${key}@example.com`,
        },
        userId,
        fraudConfig: { [key]: { enabled: true } },
        db: testDb,
        persistEvents: false,
      });

      expect(result.action).toBe('pass');
      expect(result.signals.find((s) => s.adapter === key)).toBeUndefined();
    });
  });

  // ─── 8. resolveAction severity ordering ───────────────────────────────────

  describe('resolveAction severity', () => {
    it('flag < hold < block — highest severity wins', async () => {
      const { resolveAction } = await getRegistry();

      expect(
        resolveAction([
          { adapter: 'a', action: 'flag', codes: [] },
          { adapter: 'b', action: 'block', codes: [] },
          { adapter: 'c', action: 'hold', codes: [] },
        ]),
      ).toBe('block');
    });

    it('empty signals → pass', async () => {
      const { resolveAction } = await getRegistry();
      expect(resolveAction([])).toBe('pass');
    });

    it('only flag signals → flag', async () => {
      const { resolveAction } = await getRegistry();
      expect(
        resolveAction([
          { adapter: 'x', action: 'flag', codes: [] },
          { adapter: 'y', action: 'flag', codes: [] },
        ]),
      ).toBe('flag');
    });
  });

  // ─── 9. applyDowngrade ─────────────────────────────────────────────────────

  describe('applyDowngrade', () => {
    it('downgrades block → flag when cfg.action = flag', async () => {
      const { applyDowngrade } = await getRegistry();

      const signal = { adapter: 'test', action: 'block' as const, codes: ['X'] };
      const result = applyDowngrade(signal, { enabled: true, action: 'flag' });

      expect(result.action).toBe('flag');
      expect(result.codes).toEqual(['X']);
    });

    it('does not upgrade — flag stays flag even if cfg.action = block', async () => {
      const { applyDowngrade } = await getRegistry();

      const signal = { adapter: 'test', action: 'flag' as const, codes: ['X'] };
      const result = applyDowngrade(signal, { enabled: true, action: 'block' });

      expect(result.action).toBe('flag');
    });

    it('no cfg.action → signal unchanged', async () => {
      const { applyDowngrade } = await getRegistry();

      const signal = { adapter: 'test', action: 'block' as const, codes: ['X'] };
      const result = applyDowngrade(signal, { enabled: true });

      expect(result.action).toBe('block');
    });
  });
});
