import { Workbook } from 'exceljs';
import { AUDIT_IDS_MAX, AuditContext } from '../audit/audit.entity';
import { AuditService } from '../audit/audit.service';
import { MoneyService } from './money.service';

/**
 * **엑셀 일괄 지급의 감사 줄이 업무 배치와 이어진다**는 계약.
 *
 * 이것이 조사 가설의 반증 조건 1 이다. 지금까지 엑셀 경로의 감사 줄에는 `fileRows` 같은 요약만
 * 있어, 조사자가 "이 업로드가 만든 지급이 무엇인가" 에 답할 수 없었다.
 *
 * 식별자는 `changes` **밖** 최상위 `batch` 키에 실린다 — 안에 두면 상한 초과 시 객체 전체가
 * 절단 봉투로 치환되어 절단 표시(`_idsTruncated`)까지 함께 사라진다. 범위(min/max)는 연결
 * 근거가 아니다: id 는 전역 AUTO_INCREMENT 라 동시 업로드의 값이 교차 배정된다.
 */
const TX = { __tx: true } as any;
TX.insert = () => ({ values: async () => undefined });

const ctx: AuditContext = {
  actor: { role: 'ADMIN', refId: 19, refLabel: 'admin-a', orgPath: null, personId: null },
  requestId: 'req-a',
  ip: '192.0.2.10',
  userAgent: 'test-agent',
};

const MEMBER = { memId: 4211, memMoney: 100_000, memPurchaseMoney: 0, memAgencyMoney: 100_000 };

async function xlsx(rows: unknown[][]): Promise<Express.Multer.File> {
  const wb = new Workbook();
  const ws = wb.addWorksheet('s');
  rows.forEach((r) => ws.addRow(r as any[]));
  return { buffer: Buffer.from(await wb.xlsx.writeBuffer()) } as Express.Multer.File;
}

function harness(over: Record<string, unknown> = {}) {
  const info = jest.fn();
  const audit = new AuditService({ setContext: jest.fn(), info } as any);
  let nextId = 300;
  const base: Record<string, unknown> = {
    runInTransaction: async (fn: (tx: unknown) => unknown) => fn(TX),
    getAgencyById: async () => ({ agcId: 5, agcMoney: 100_000_000 }),
    getAgencyByIdForUpdate: async () => ({ agcId: 5, agcMoney: 100_000_000 }),
    getMemberByLoginIdAndAgcId: async () => MEMBER,
    getMemberByLoginId: async () => MEMBER,
    getMemberByIdForUpdate: async () => MEMBER,
    createGrant: async () => ++nextId,
    createIssue: async () => ++nextId,
    createWallet: async () => 900,
    ...over,
  };
  const repository = new Proxy({} as any, {
    get: (_t, k: string) => (k in base ? base[k] : async () => undefined),
  });
  const service = new MoneyService(repository, {} as any, {} as any, {} as any, audit);
  const line = (action: string) =>
    info.mock.calls.map((c) => (c[0] as any).audit).find((a) => a.action === action);
  return { service, line };
}

const GRANT_HEADER = ['회원 아이디', '금액', '내용', '사유', '유효기간'];
const ISSUE_HEADER = ['구분', '회원 아이디', '기업코드 (기업회원 필수)', '금액', '내용', '사유', '유효기간'];

describe('uploadGrant — 지급 배치 식별자가 감사 줄에 실린다', () => {
  it('행마다 만든 mngId 가 batch 키로 나간다', async () => {
    const { service, line } = harness();
    const file = await xlsx([
      GRANT_HEADER,
      ['corp_user1', 1000, '지급', '사유', ''],
      ['corp_user2', 2000, '지급', '사유', ''],
    ]);
    await service.uploadGrant(19, file, 5, 'CORRECTION', ctx);
    const l = line('money.grant.upload');
    expect(l.batch).toEqual({ ids: [301, 302], idsTotal: 2 });
  });

  /**
   * `changes` 안에 두면 상한 초과 시 절단 봉투가 배치 식별자와 절단 표시까지 함께 지운다.
   */
  it('배치 식별자는 changes 안이 아니라 최상위 batch 키에 있다', async () => {
    const { service, line } = harness();
    const file = await xlsx([GRANT_HEADER, ['corp_user1', 1000, '지급', '사유', '']]);
    await service.uploadGrant(19, file, 5, 'CORRECTION', ctx);
    const l = line('money.grant.upload');
    expect(JSON.stringify(l.changes)).not.toContain('301');
    expect((l.batch.ids as number[])[0]).toBe(301);
  });

  it('상한을 넘는 업로드는 잘린 사실을 값에 남긴다 — 부분 재구성임을 알 수 있다', async () => {
    const { service, line } = harness();
    const rows: unknown[][] = [GRANT_HEADER];
    for (let i = 0; i < AUDIT_IDS_MAX + 1; i++) rows.push([`corp_user${i}`, 1000, '지급', '사유', '']);
    await service.uploadGrant(19, await xlsx(rows), 5, 'CORRECTION', ctx);
    const l = line('money.grant.upload');
    expect((l.batch.ids as number[]).length).toBe(AUDIT_IDS_MAX);
    expect(l.batch.idsTotal).toBe(AUDIT_IDS_MAX + 1);
    expect(l.batch._idsTruncated).toBe(true);
  });
});

describe('uploadIssue — 적립 배치 식별자가 감사 줄에 실린다', () => {
  it('행마다 만든 mniId 가 batch 키로 나간다', async () => {
    const { service, line } = harness();
    const file = await xlsx([
      ISSUE_HEADER,
      ['일반', 'general_user1', '', 1000, '적립', '사유', ''],
      ['일반', 'general_user2', '', 2000, '적립', '사유', ''],
    ]);
    await service.uploadIssue(19, file, 'CORRECTION', ctx);
    const l = line('money.issue.upload');
    expect(l.batch).toEqual({ ids: [301, 302], idsTotal: 2 });
  });

  it('배치 식별자는 changes 안이 아니라 최상위 batch 키에 있다', async () => {
    const { service, line } = harness();
    const file = await xlsx([ISSUE_HEADER, ['일반', 'general_user1', '', 1000, '적립', '사유', '']]);
    await service.uploadIssue(19, file, 'CORRECTION', ctx);
    const l = line('money.issue.upload');
    expect(JSON.stringify(l.changes)).not.toContain('301');
    expect((l.batch.ids as number[])[0]).toBe(301);
  });

  /**
   * 두 업로드 경로는 `if (id) ids.push(id)` 로 모은다 — 저장소가 id 를 안 돌려준 행은 조용히
   * 빠졌고, `idsTotal` 이 `targetCount` 보다 작아도 그 사실을 말하는 값이 없었다. 조사자가
   * 부분 목록을 전체로 읽는 자리다.
   */
  it('id 를 못 받은 행이 있으면 감사 줄이 그 사실을 말한다', async () => {
    let n = 0;
    //* 두 번째 행만 id 를 못 받는다 — 저장소가 삽입 id 를 못 돌려주는 상황의 대역이다.
    const { service, line } = harness({ createIssue: async () => (++n === 2 ? undefined : 300 + n) });
    const file = await xlsx([
      ISSUE_HEADER,
      ['일반', 'general_user1', '', 1000, '적립', '사유', ''],
      ['일반', 'general_user2', '', 2000, '적립', '사유', ''],
      ['일반', 'general_user3', '', 3000, '적립', '사유', ''],
    ]);
    await service.uploadIssue(19, file, 'CORRECTION', ctx);
    const l = line('money.issue.upload');
    expect(l.batch.idsTotal).toBe(2);
    expect(l.batch._idsMissing).toBe(1);
    //* 상한 절단과 **다른 사건**이다 — 하나는 우리가 잘랐고 하나는 애초에 안 왔다.
    expect(l.batch._idsTruncated).toBeUndefined();
  });

  it('전 행이 id 를 돌려주면 유실 표시가 없다', async () => {
    const { service, line } = harness();
    const file = await xlsx([
      ISSUE_HEADER,
      ['일반', 'general_user1', '', 1000, '적립', '사유', ''],
      ['일반', 'general_user2', '', 2000, '적립', '사유', ''],
    ]);
    await service.uploadIssue(19, file, 'CORRECTION', ctx);
    expect('_idsMissing' in line('money.issue.upload').batch).toBe(false);
  });

  it('dryRun 은 아무 줄도 내지 않는다 — 적용하지 않은 것을 기록으로 남기지 않는다', async () => {
    const { service, line } = harness();
    const file = await xlsx([ISSUE_HEADER, ['일반', 'general_user1', '', 1000, '적립', '사유', '']]);
    await service.uploadIssue(19, file, 'CORRECTION', ctx, true);
    expect(line('money.issue.upload')).toBeUndefined();
  });
});
