import { describe, test, expect, beforeEach } from 'vitest';
import { openDatabase } from '@insta-monitor/db';
import type { Database } from '@insta-monitor/db';
import type { ScrapedProfile } from '@insta-monitor/collector';
import { IgRateLimitError } from '@insta-monitor/collector';
import { runDailyLight, backfillNext, FAIL_ABORT } from './index.js';
import type { ProfileSource } from './index.js';

const prof = (u: string): ScrapedProfile => ({ username: u, displayName: u, followerCount: 0, postCount: 0, posts: [] });
const opts = { date: '2026-06-24', collectedAt: '2026-06-24T11:00:00Z' };
let db: Database;
beforeEach(() => { db = openDatabase(':memory:'); });

describe('runDailyLight', () => {
  test('collects only refresh-due auto accounts in light mode', async () => {
    const a = db.accounts.create({ username: 'a', autoCollect: true });
    db.accounts.create({ username: 'b' }); // auto off
    db.accounts.setBackfillState(a, 'done');
    const seen: Array<{ u: string; mode?: string }> = [];
    const src: ProfileSource = { collectProfile: async (u, o) => { seen.push({ u, mode: o?.mode }); return prof(u); } };
    const res = await runDailyLight(db, src, opts);
    expect(res.map((r) => r.username)).toEqual(['a']);
    expect(seen).toEqual([{ u: 'a', mode: 'light' }]);
  });

  test('caps long-tail to `limit` accounts so a large fleet staggers over days', async () => {
    for (const u of ['a', 'b', 'c']) db.accounts.create({ username: u, autoCollect: true });
    const seen: string[] = [];
    const src: ProfileSource = { collectProfile: async (u) => { seen.push(u); return prof(u); } };
    const res = await runDailyLight(db, src, opts, undefined, 2);
    expect(res).toHaveLength(2);
    expect(seen).toHaveLength(2);
  });

  test('default long-tail budget collects a full <=500 fleet in one day (no rotation)', async () => {
    for (let i = 0; i < 130; i += 1) db.accounts.create({ username: `u${i}`, autoCollect: true });
    const src: ProfileSource = { collectProfile: async (u) => prof(u) };
    const res = await runDailyLight(db, src, opts); // default budget — was 120, now 500
    expect(res).toHaveLength(130); // all 130 same-day; would cap at 120 under the old default
  });

  test('always collects priority channels, plus long-tail up to the limit', async () => {
    const p1 = db.accounts.create({ username: 'p1', autoCollect: true }); db.accounts.setPriority(p1, true);
    const p2 = db.accounts.create({ username: 'p2', autoCollect: true }); db.accounts.setPriority(p2, true);
    for (const u of ['t1', 't2', 't3']) db.accounts.create({ username: u, autoCollect: true });
    const seen: string[] = [];
    const src: ProfileSource = { collectProfile: async (u) => { seen.push(u); return prof(u); } };
    const res = await runDailyLight(db, src, opts, undefined, 1); // long-tail budget = 1
    expect(res).toHaveLength(3); // both priority + 1 long-tail
    expect(seen).toEqual(expect.arrayContaining(['p1', 'p2']));
    expect(seen.filter((u) => u.startsWith('t'))).toHaveLength(1);
  });

  test('onAfterEach fires once after each collected account (incremental 떡상 alerts)', async () => {
    for (const u of ['a', 'b']) db.accounts.create({ username: u, autoCollect: true });
    const fired: string[] = [];
    const src: ProfileSource = { collectProfile: async (u) => prof(u) };
    await runDailyLight(db, src, opts, undefined, 500, undefined, undefined, (r) => { fired.push(r.username); });
    expect(fired).toHaveLength(2);
  });

  test('aborts the pass after consecutive rate-limits instead of feeding the throttle', async () => {
    for (let i = 0; i < 10; i += 1) db.accounts.create({ username: `u${i}`, autoCollect: true });
    const src: ProfileSource = { collectProfile: async () => { throw new IgRateLimitError('throttled'); } };
    const res = await runDailyLight(db, src, opts);
    expect(res).toHaveLength(4); // RATE_LIMIT_ABORT — stops after 4, doesn't hammer all 10
    expect(res.every((r) => r.rateLimited)).toBe(true);
  });

  test('aborts the pass after consecutive failures even without a rate-limit signal (soft-block)', async () => {
    for (let i = 0; i < 10; i += 1) db.accounts.create({ username: `u${i}`, autoCollect: true });
    const src: ProfileSource = { collectProfile: async () => { throw new Error('no timeline (soft block)'); } };
    const res = await runDailyLight(db, src, opts);
    expect(res).toHaveLength(FAIL_ABORT); // stop grinding all 10 even without a rate-limit signal
    expect(res.every((r) => r.status === 'error' && !r.rateLimited)).toBe(true);
  });
});

describe('backfillNext', () => {
  test('deep-collects one pending account and marks it done', async () => {
    db.accounts.create({ username: 'a' }); // pending by default
    const src: ProfileSource = { collectProfile: async (u) => prof(u) };
    const out = await backfillNext(db, src, opts);
    expect(out).toMatchObject({ ran: true, username: 'a', rateLimited: false });
    expect(db.accounts.getByUsername('a')!.backfillState).toBe('done');
  });

  test('rate-limit sets the flag without throwing and leaves it pending', async () => {
    db.accounts.create({ username: 'a' });
    const src: ProfileSource = { collectProfile: async () => { throw new IgRateLimitError('throttled'); } };
    const out = await backfillNext(db, src, opts);
    expect(out.rateLimited).toBe(true);
    expect(db.accounts.getByUsername('a')!.backfillState).toBe('pending');
  });

  test('marks unavailable after 3 consecutive failures', async () => {
    const id = db.accounts.create({ username: 'a' });
    const src: ProfileSource = { collectProfile: async () => { throw new Error('nope'); } };
    await backfillNext(db, src, opts);
    await backfillNext(db, src, opts);
    await backfillNext(db, src, opts);
    expect(db.accounts.getById(id)!.backfillState).toBe('unavailable');
  });

  test('no pending accounts -> ran false', async () => {
    const id = db.accounts.create({ username: 'a' });
    db.accounts.setBackfillState(id, 'done');
    const src: ProfileSource = { collectProfile: async (u) => prof(u) };
    expect(await backfillNext(db, src, opts)).toMatchObject({ ran: false });
  });
});

describe('runDailyLight: stop (AbortSignal)', () => {
  test('abort between accounts → stops the pass, keeps only pre-abort results', async () => {
    for (const u of ['a', 'b', 'c', 'd']) db.accounts.create({ username: u, autoCollect: true });
    const ac = new AbortController();
    let n = 0;
    const src: ProfileSource = {
      collectProfile: async (u) => { n += 1; if (n === 2) ac.abort(); return prof(u); },
    };
    const res = await runDailyLight(db, src, opts, undefined, 500, undefined, ac.signal);
    expect(res.length).toBe(1); // #2 dropped on abort, #3/#4 never run
  });
  test('already-aborted signal → collects nothing', async () => {
    for (const u of ['a', 'b']) db.accounts.create({ username: u, autoCollect: true });
    const ac = new AbortController(); ac.abort();
    const seen: string[] = [];
    const src: ProfileSource = { collectProfile: async (u) => { seen.push(u); return prof(u); } };
    const res = await runDailyLight(db, src, opts, undefined, 500, undefined, ac.signal);
    expect(res).toEqual([]);
    expect(seen).toEqual([]);
  });
});

describe('runDailyLight: weekly not_found liveness probe', () => {
  test('old not_found accounts are probed LAST and revived to pending on success', async () => {
    const dead = db.accounts.create({ username: 'dead', autoCollect: true });
    db.accounts.setBackfillState(dead, 'not_found');
    db.accounts.markRefreshed(dead, '2026-06-10T00:00:00Z'); // >7d before opts.collectedAt
    db.accounts.create({ username: 'live', autoCollect: true });
    const seen: string[] = [];
    const src: ProfileSource = { collectProfile: async (u) => { seen.push(u); return prof(u); } };
    await runDailyLight(db, src, opts);
    expect(seen).toEqual(['live', 'dead']); // probe appended after the real pass
    expect(db.accounts.getById(dead)!.backfillState).toBe('pending'); // revived
  });

  test('recently-probed not_found accounts are skipped (weekly cadence)', async () => {
    const dead = db.accounts.create({ username: 'dead', autoCollect: true });
    db.accounts.setBackfillState(dead, 'not_found');
    db.accounts.markRefreshed(dead, '2026-06-23T00:00:00Z'); // 1d ago
    const src: ProfileSource = { collectProfile: async (u) => prof(u) };
    const res = await runDailyLight(db, src, opts);
    expect(res).toHaveLength(0);
  });
});
