/**
 * Account-deletion purge job tests.
 *
 * The destructive selection + delete logic is extracted into pure functions
 * (selectExpiredDeletions, runAccountDeletionPurge) so it can be tested with an
 * injected clock and an injected deleter — no real DB, no real wall clock.
 *
 * SAFETY is the focus: the purge must delete ONLY accounts whose
 * deletionScheduledAt is in the PAST (opted in + grace elapsed), never an
 * account with a future date, no date, an unparseable date, or one that is
 * currently online. And it must be idempotent.
 */
import { describe, it, expect, vi } from 'vitest';
import {
  selectExpiredDeletions,
  runAccountDeletionPurge,
  type DeletionCandidate,
} from '../../../src/services/background/jobs.js';

const NOW = 1_700_000_000_000; // fixed clock
const DAY = 24 * 60 * 60 * 1000;

function iso(ms: number): string {
  return new Date(ms).toISOString();
}

describe('selectExpiredDeletions', () => {
  it('selects only past-due, opted-in, offline accounts', () => {
    const candidates: DeletionCandidate[] = [
      { userId: 'past-due', deletionScheduledAt: iso(NOW - DAY) },
      { userId: 'future', deletionScheduledAt: iso(NOW + DAY) },
      { userId: 'no-date' },
      { userId: 'null-date', deletionScheduledAt: null },
      { userId: 'bad-date', deletionScheduledAt: 'not-a-date' },
      { userId: 'online', deletionScheduledAt: iso(NOW - DAY), connected: true },
      { userId: 'exactly-now', deletionScheduledAt: iso(NOW) },
    ];

    const selected = selectExpiredDeletions(candidates, NOW).map((c) => c.userId);

    expect(selected).toContain('past-due');
    expect(selected).toContain('exactly-now'); // <= now is past-due
    expect(selected).not.toContain('future');
    expect(selected).not.toContain('no-date');
    expect(selected).not.toContain('null-date');
    expect(selected).not.toContain('bad-date');
    expect(selected).not.toContain('online');
    expect(selected).toHaveLength(2);
  });

  it('returns nothing for an empty list', () => {
    expect(selectExpiredDeletions([], NOW)).toEqual([]);
  });
});

describe('runAccountDeletionPurge', () => {
  it('deletes a player whose deletionScheduledAt is in the PAST', async () => {
    const deleted: string[] = [];
    const deleter = vi.fn(async (id: string) => {
      deleted.push(id);
    });

    const result = await runAccountDeletionPurge(
      [{ userId: 'expired', deletionScheduledAt: iso(NOW - DAY) }],
      NOW,
      deleter
    );

    expect(deleter).toHaveBeenCalledExactlyOnceWith('expired');
    expect(deleted).toEqual(['expired']);
    expect(result).toMatchObject({ selected: 1, deleted: 1, failed: 0 });
  });

  it('does NOT delete a player with a FUTURE date or NO date', async () => {
    const deleter = vi.fn(async () => undefined);

    const result = await runAccountDeletionPurge(
      [
        { userId: 'future', deletionScheduledAt: iso(NOW + 5 * DAY) },
        { userId: 'no-date' },
        { userId: 'null-date', deletionScheduledAt: null },
      ],
      NOW,
      deleter
    );

    expect(deleter).not.toHaveBeenCalled();
    expect(result).toMatchObject({ selected: 0, deleted: 0, failed: 0 });
  });

  it('never deletes an online account even if past-due', async () => {
    const deleter = vi.fn(async () => undefined);

    await runAccountDeletionPurge(
      [{ userId: 'online', deletionScheduledAt: iso(NOW - DAY), connected: true }],
      NOW,
      deleter
    );

    expect(deleter).not.toHaveBeenCalled();
  });

  it('is idempotent: a second run after deletion is a safe no-op', async () => {
    // Simulate a DB-backed store: candidate gone after first delete.
    const store = new Map<string, DeletionCandidate>([
      ['expired', { userId: 'expired', deletionScheduledAt: iso(NOW - DAY) }],
    ]);
    const deleter = vi.fn(async (id: string) => {
      store.delete(id);
    });

    const first = await runAccountDeletionPurge([...store.values()], NOW, deleter);
    expect(first).toMatchObject({ selected: 1, deleted: 1, failed: 0 });

    const second = await runAccountDeletionPurge([...store.values()], NOW, deleter);
    expect(second).toMatchObject({ selected: 0, deleted: 0, failed: 0 });
    expect(deleter).toHaveBeenCalledTimes(1);
  });

  it('tallies a deleter failure without aborting the sweep', async () => {
    const deleter = vi.fn(async (id: string) => {
      if (id === 'boom') throw new Error('db down');
    });

    const result = await runAccountDeletionPurge(
      [
        { userId: 'boom', deletionScheduledAt: iso(NOW - DAY) },
        { userId: 'ok', deletionScheduledAt: iso(NOW - DAY) },
      ],
      NOW,
      deleter
    );

    expect(result.selected).toBe(2);
    expect(result.deleted).toBe(1);
    expect(result.failed).toBe(1);
  });
});
