import { afterEach, describe, expect, it } from 'vitest';
import { checkAccounts } from '../check-accounts.js';
import type { AccountRecord } from '../check-accounts.js';
import { _resetRunState, startRun, type ScrapeTask } from '../scrape-runner.js';
import type { ServerMessage } from '../../shared/ws-protocol.js';

const poalimPayload = {
  accountNumber: 100000,
  branchNumber: 600,
  bankNumber: 12,
};

const isracardPayload = {
  Header: { Status: '1', Message: null },
  CardsTransactionsListBean: {
    cardNumberList: ['CARD 9876', 'CARD 4545'],
    Index0: {
      '@AllCards': 'AllCards',
      CurrentCardTransactions: [
        { txnIsrael: null, txnAbroad: null },
        { txnIsrael: null, txnAbroad: null },
      ],
    },
  },
};

const calPayload = [
  { card: 'ACC-1', month: '2024-01', transactions: [] },
  { card: 'ACC-2', month: '2024-01', transactions: [] },
];

const maxPayload = [
  { accountNumber: '7', txns: [] },
  { accountNumber: '8', txns: [] },
];

const discountPayload = [
  { accountNumber: 'ACC-001', month: '2024-01', balance: 5000, transactions: [] },
  { accountNumber: 'ACC-002', month: '2024-01', balance: 3000, transactions: [] },
];

function makeRecord(
  sourceType: AccountRecord['sourceType'],
  accountNumber: string,
  status: AccountRecord['status'] = 'accepted',
): AccountRecord {
  return {
    id: `${sourceType}-${accountNumber}`,
    sourceId: 'src-1',
    sourceType,
    accountNumber,
    status,
  };
}

describe('checkAccounts — poalim', () => {
  it('accepted when account is in known list with accepted status', () => {
    const known = [makeRecord('poalim', '600-100000', 'accepted')];
    const result = checkAccounts('poalim', poalimPayload, known);
    expect(result.accepted).toEqual(['600-100000']);
    expect(result.ignored).toEqual([]);
    expect(result.unknown).toEqual([]);
  });

  it('unknown when account has pending status', () => {
    const known = [makeRecord('poalim', '600-100000', 'pending')];
    const result = checkAccounts('poalim', poalimPayload, known);
    expect(result.accepted).toEqual([]);
    expect(result.unknown).toEqual(['600-100000']);
  });

  it('unknown when account is not in known list', () => {
    const result = checkAccounts('poalim', poalimPayload, []);
    expect(result.unknown).toEqual(['600-100000']);
    expect(result.accepted).toEqual([]);
  });

  it('ignored when account status is ignored', () => {
    const known = [makeRecord('poalim', '600-100000', 'ignored')];
    const result = checkAccounts('poalim', poalimPayload, known);
    expect(result.ignored).toEqual(['600-100000']);
    expect(result.accepted).toEqual([]);
    expect(result.unknown).toEqual([]);
  });

  it('does not match account from a different sourceType', () => {
    const known = [makeRecord('discount', '600-100000', 'accepted')];
    const result = checkAccounts('poalim', poalimPayload, known);
    expect(result.unknown).toEqual(['600-100000']);
  });
});

describe('checkAccounts — discount', () => {
  it('classifies accountNumber identifiers from payload', () => {
    const known = [
      makeRecord('discount', 'ACC-001', 'accepted'),
      makeRecord('discount', 'ACC-002', 'ignored'),
    ];
    const result = checkAccounts('discount', discountPayload, known);
    expect(result.accepted).toEqual(['ACC-001']);
    expect(result.ignored).toEqual(['ACC-002']);
    expect(result.unknown).toEqual([]);
  });

  it('returns unknown for unrecognised accountNumber', () => {
    const result = checkAccounts('discount', discountPayload, []);
    expect(result.unknown).toEqual(['ACC-001', 'ACC-002']);
    expect(result.accepted).toEqual([]);
  });

  it('deduplicates accountNumber across multiple months for the same account', () => {
    const sameAccountTwoMonths = [
      { accountNumber: 'ACC-001', month: '2024-01', balance: 5000, transactions: [] },
      { accountNumber: 'ACC-001', month: '2024-02', balance: 4800, transactions: [] },
    ];
    const result = checkAccounts('discount', sameAccountTwoMonths, []);
    expect(result.unknown).toEqual(['ACC-001']);
  });
});

describe('checkAccounts — isracard / amex', () => {
  it('classifies each card from CurrentCardTransactions', () => {
    const known = [
      makeRecord('isracard', '4545', 'accepted'),
      makeRecord('isracard', '9876', 'ignored'),
    ];
    const result = checkAccounts('isracard', isracardPayload, known);
    expect(result.accepted).toEqual(['4545']);
    expect(result.ignored).toEqual(['9876']);
    expect(result.unknown).toEqual([]);
  });

  it('returns unknown cards not in known list', () => {
    const known = [makeRecord('isracard', '9876', 'accepted')];
    const result = checkAccounts('isracard', isracardPayload, known);
    expect(result.unknown).toEqual(['4545']);
  });

  it('works the same for amex payload type', () => {
    const known = [makeRecord('amex', '4545', 'ignored')];
    const result = checkAccounts('amex', isracardPayload, known);
    expect(result.ignored).toEqual(['4545']);
    expect(result.unknown).toEqual(['9876']);
  });

  it('returns empty when there are no cards in payload', () => {
    const emptyPayload = {
      Header: { Status: '1', Message: null },
      CardsTransactionsListBean: {
        cardNumberList: [],
        Index0: { '@AllCards': 'AllCards', CurrentCardTransactions: [] },
      },
    };
    const result = checkAccounts('isracard', emptyPayload, []);
    expect(result).toEqual({ accepted: [], ignored: [], unknown: [] });
  });

  it('collects identifiers from cardNumberList', () => {
    const multiIndexPayload = {
      Header: { Status: '1', Message: null },
      CardsTransactionsListBean: {
        cardNumberList: ['CARD 1111', 'CARD 2222', 'CARD 3333'],
        Index0: {
          '@AllCards': 'AllCards',
          CurrentCardTransactions: [{ '@cardTransactions': 'CARD-A' }],
        },
        Index1: {
          '@AllCards': 'AllCards',
          CurrentCardTransactions: [{ '@cardTransactions': 'CARD-B' }],
        },
        Index2: {
          '@AllCards': 'AllCards',
          CurrentCardTransactions: [{ '@cardTransactions': 'CARD-C' }],
        },
      },
    };
    const result = checkAccounts('isracard', multiIndexPayload, []);
    expect(result.unknown.sort()).toEqual(['1111', '2222', '3333']);
  });

  it('ignores non-Index* keys in CardsTransactionsListBean', () => {
    const payloadWithExtraKeys = {
      Header: { Status: '1', Message: null },
      CardsTransactionsListBean: {
        cardNumberList: ['CARD 0101'],
        Index0: {
          '@AllCards': 'AllCards',
          CurrentCardTransactions: [],
        },
        cardIdx: '0',
        card0: { some: 'data' },
      },
    };
    const result = checkAccounts('isracard', payloadWithExtraKeys, []);
    expect(result.unknown).toEqual(['0101']);
  });
});

describe('checkAccounts — cal', () => {
  it('classifies card identifiers', () => {
    const known = [
      makeRecord('cal', 'ACC-1', 'accepted'),
      makeRecord('cal', 'ACC-2', 'ignored'),
    ];
    const result = checkAccounts('cal', calPayload, known);
    expect(result.accepted).toEqual(['ACC-1']);
    expect(result.ignored).toEqual(['ACC-2']);
    expect(result.unknown).toEqual([]);
  });

  it('returns unknown for unrecognised card', () => {
    const result = checkAccounts('cal', calPayload, []);
    expect(result.unknown).toEqual(['ACC-1', 'ACC-2']);
  });
});

describe('checkAccounts — max', () => {
  it('classifies unique cardIndex values from transactions', () => {
    const known = [
      makeRecord('max', '7', 'accepted'),
      makeRecord('max', '8', 'ignored'),
    ];
    const result = checkAccounts('max', maxPayload, known);
    expect(result.accepted).toEqual(['7']);
    expect(result.ignored).toEqual(['8']);
    expect(result.unknown).toEqual([]);
  });

  it('deduplicates accountNumber across multiple entries for the same card', () => {
    const twoEntriesSameCard = [
      { accountNumber: '7', txns: [] },
      { accountNumber: '7', txns: [] },
    ];
    const result = checkAccounts('max', twoEntriesSameCard, []);
    expect(result.unknown).toEqual(['7']);
  });

  it('returns empty arrays when there are no accounts', () => {
    const result = checkAccounts('max', [], []);
    expect(result).toEqual({ accepted: [], ignored: [], unknown: [] });
  });
});

describe('runner integration — task-blocked on unknown accounts', () => {
  afterEach(() => {
    _resetRunState();
  });

  it('emits task-blocked (not a crash) when checkAccounts finds unknown accounts', async () => {
    const events: ServerMessage[] = [];
    const emit = (msg: ServerMessage) => events.push(msg);

    const task: ScrapeTask = {
      sourceId: 'poalim-src',
      nickname: 'poalim-src',
      type: 'poalim',
      run: async () => {
        const check = checkAccounts('poalim', poalimPayload, []);
        if (check.unknown.length > 0) {
          emit({ type: 'task-blocked', sourceId: 'poalim-src', sourceType: 'poalim', unknownAccounts: check.unknown });
          return { inserted: 0, skipped: 0, insertedIds: [], changedTransactions: [], insertedTransactions: [] };
        }
        return { inserted: 1, skipped: 0, insertedIds: ['x'], changedTransactions: [], insertedTransactions: [] };
      },
    };

    await startRun([task], false, emit);

    const blocked = events.find(e => e.type === 'task-blocked');
    expect(blocked).toBeTruthy();
    expect((blocked as { unknownAccounts: string[] }).unknownAccounts).toContain('600-100000');
    expect(events.at(-1)).toMatchObject({ type: 'run-complete', totalInserted: 0, totalSkipped: 0 });
  });

  it('continues remaining tasks after a blocked task', async () => {
    const events: ServerMessage[] = [];
    const emit = (msg: ServerMessage) => events.push(msg);

    const blockedTask: ScrapeTask = {
      sourceId: 'src-1',
      nickname: 'src-1',
      type: 'poalim',
      run: async () => {
        const check = checkAccounts('poalim', poalimPayload, []);
        if (check.unknown.length > 0) {
          emit({ type: 'task-blocked', sourceId: 'src-1', sourceType: 'poalim', unknownAccounts: check.unknown });
          return { inserted: 0, skipped: 0, insertedIds: [], changedTransactions: [], insertedTransactions: [] };
        }
        return { inserted: 1, skipped: 0, insertedIds: ['x'], changedTransactions: [], insertedTransactions: [] };
      },
    };

    const normalTask: ScrapeTask = {
      sourceId: 'src-2',
      nickname: 'src-2',
      type: 'poalim',
      run: async () => ({ inserted: 2, skipped: 1, insertedIds: ['a', 'b'], changedTransactions: [], insertedTransactions: [] }),
    };

    await startRun([blockedTask, normalTask], false, emit);

    expect(events.some(e => e.type === 'task-blocked')).toBe(true);
    expect(events.some(e => e.type === 'task-done' && 'sourceId' in e && e.sourceId === 'src-2')).toBe(true);
    expect(events.at(-1)).toMatchObject({ type: 'run-complete', totalInserted: 2, totalSkipped: 1 });
  });
});
