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

/**
 * **감사 행의 잔액 전·후가 적용 금액과 산술로 맞는다** — 계획 M8 D6 · Task 5.
 *
 * 키의 존재가 아니라 **차**를 본다. 게이트 `#6` 이 산술을 하나씩 깬 사본 일곱을 돌린다 — 단건을 합계로 ·
 * 합계 후를 전으로 · 반복 회원의 첫 전 잔액을 덮음 · 적립 후 잔액 미반영 · 회수 후 잔액 미반영 · 엑셀 지급
 * 기업 후 잔액을 전으로 · 승인 기업 전 잔액을 후로.
 *
 * 저장소 스텁은 회원·기업 잔액을 **호출마다 갱신한다.** 고정값을 돌려주면 같은 회원의 두 번째 읽기가 첫
 * 반영 전 잔액이 되어, 반복 회원을 접는 규칙(D6)이 틀려도 이 spec 이 모른다.
 */

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

const A = { memId: 4211, memLoginId: 'user-a' };
const B = { memId: 4212, memLoginId: 'user-b' };

function harness(balances: Record<number, number>, agencyMoney = 1_000_000) {
  const members = new Map(
    [A, B].map((m) => [
      m.memId,
      {
        ...m,
        agcId: 5,
        memMoney: balances[m.memId] ?? 0,
        memPurchaseMoney: 0,
        memAgencyMoney: 1_000_000,
      },
    ]),
  );
  const agency = { agcId: 5, agcCode: 'AGC1', agcMoney: agencyMoney };
  const byId = async (memId: number) => ({ ...members.get(memId)! });
  const byLogin = async (login: string) => ({
    ...[...members.values()].find((m) => m.memLoginId === login)!,
  });
  const rows: Record<string, any>[] = [];
  const tx = {
    insert: () => ({
      values: async (row: Record<string, unknown>) => {
        rows.push(row);
      },
    }),
  };
  const base: Record<string, unknown> = {
    runInTransaction: async (fn: (t: unknown) => unknown) => fn(tx),
    getAgencyById: async () => ({ ...agency }),
    getAgencyByIdForUpdate: async () => ({ ...agency }),
    updateAgencyMoney: async (_agcId: number, money: number) => {
      agency.agcMoney = money;
    },
    getMemberById: byId,
    getMemberByIdForUpdate: byId,
    getMemberByLoginId: byLogin,
    getMemberByLoginIdAndAgcId: byLogin,
    updateMemberMoney: async (
      memId: number,
      memMoney: number,
      memPurchaseMoney: number,
      memAgencyMoney: number,
    ) => {
      Object.assign(members.get(memId)!, {
        memMoney,
        memPurchaseMoney,
        memAgencyMoney,
      });
    },
    getSourcesForDeduct: async () => [
      { msrId: 1, msrMoneyType: 'BASIC', msrRemainingAmount: 1_000_000 },
    ],
    isMemberInAgencyScope: async () => true,
    createIssue: async () => 4001,
    createGrant: async () => 5001,
    createWallet: async () => 6001,
    getChargeRequestById: async () => ({
      mcrId: 77,
      agcId: 5,
      mcrAmount: 50_000,
      mcrStatus: 'REQUESTED',
      mcrTitle: 't',
      mcrReason: null,
    }),
    //* 저장소 승인 본문의 산술(선점 뒤 FOR UPDATE 잔액 + 요청 금액)을 같은 상태 위에서 흉내 낸다.
    approveChargeRequest: async (request: { mcrAmount: number }) => {
      const balanceBefore = agency.agcMoney;
      agency.agcMoney += request.mcrAmount;
      return { processed: true, balanceBefore, newBalance: agency.agcMoney };
    },
  };
  const repository = new Proxy({} as any, {
    get: (_t, k: string) => (k in base ? base[k] : async () => undefined),
  });
  const audit = new AuditService({
    setContext: jest.fn(),
    info: jest.fn(),
    error: jest.fn(),
  } as any);
  const service = new MoneyService(
    repository,
    {} as any,
    {} as any,
    {} as any,
    audit,
  );
  const changes = () => {
    expect(rows).toHaveLength(1);
    return JSON.parse(rows[0].audChanges);
  };
  return { service, changes };
}

const issue = (memberIds: number[], mniAmount: number) =>
  ({
    agcId: 5,
    mniTargetType: 'SPECIFIC',
    mniMemberType: 'NORMAL',
    memberIds,
    mniType: mniAmount > 0 ? 'EARN' : 'EARN_RECOVERY',
    mniTitle: 'title',
    mniReason: null,
    mniAmount,
    reasonCode: 'CORRECTION',
  }) as any;

async function xlsx(header: unknown[], data: unknown[][]): Promise<any> {
  const wb = new Workbook();
  const sheet = wb.addWorksheet('s');
  sheet.addRow(header);
  data.forEach((r) => sheet.addRow(r));
  return { buffer: Buffer.from(await wb.xlsx.writeBuffer()) };
}

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

describe('회원머니 발행 — 회원 잔액 전·후(기본머니)', () => {
  it('단건 적립은 그 회원의 전·후이고 차가 단가다', async () => {
    const { service, changes } = harness({ [A.memId]: 100 });
    await service.createIssue(19, issue([A.memId], 1000), ctx);
    const c = changes();
    expect(c.memberMoneyBefore).toBe(100);
    expect(c.memberMoneyAfter).toBe(1100);
    expect(c.memberMoneyAfter - c.memberMoneyBefore).toBe(1000);
    expect(c).not.toHaveProperty('memberMoneySumBefore');
  });

  it('두 명 적립은 합계 전·후이고 차가 단가 × 2 이며 단건 키가 없다', async () => {
    const { service, changes } = harness({ [A.memId]: 100, [B.memId]: 200 });
    await service.createIssue(19, issue([A.memId, B.memId], 1000), ctx);
    const c = changes();
    expect(c.memberMoneySumBefore).toBe(300);
    expect(c.memberMoneySumAfter - c.memberMoneySumBefore).toBe(2000);
    expect(c).not.toHaveProperty('memberMoneyBefore');
    expect(c).not.toHaveProperty('memberMoneyAfter');
  });

  it('단건 회수는 차가 −단가다', async () => {
    const { service, changes } = harness({ [A.memId]: 1000 });
    await service.createIssue(19, issue([A.memId], -300), ctx);
    const c = changes();
    expect(c.memberMoneyBefore).toBe(1000);
    expect(c.memberMoneyAfter - c.memberMoneyBefore).toBe(-300);
  });
});

describe('엑셀 발행 — 회원별로 접은 뒤 단건·합계', () => {
  it('다른 회원 두 행은 합계 차가 행 금액의 합이다', async () => {
    const { service, changes } = harness({ [A.memId]: 100, [B.memId]: 200 });
    const file = await xlsx(ISSUE_HEADER, [
      ['일반', A.memLoginId, '', 100, '내용', '사유', ''],
      ['일반', B.memLoginId, '', 200, '내용', '사유', ''],
    ]);
    await service.uploadIssue(19, file, 'EVENT', ctx);
    const c = changes();
    expect(c.memberMoneySumBefore).toBe(300);
    expect(c.memberMoneySumAfter - c.memberMoneySumBefore).toBe(300);
  });

  it('같은 회원 두 행(잔액 100 에 +10·+20)은 한 명의 100→130 이고 합계 키가 없다', async () => {
    const { service, changes } = harness({ [A.memId]: 100 });
    const file = await xlsx(ISSUE_HEADER, [
      ['일반', A.memLoginId, '', 10, '내용', '사유', ''],
      ['일반', A.memLoginId, '', 20, '내용', '사유', ''],
    ]);
    await service.uploadIssue(19, file, 'EVENT', ctx);
    const c = changes();
    expect(c.memberMoneyBefore).toBe(100);
    expect(c.memberMoneyAfter).toBe(130);
    expect(c).not.toHaveProperty('memberMoneySumBefore');
  });
});

describe('기업 잔액 전·후', () => {
  it('엑셀 지급은 기업 전 − 후가 지급 합이다', async () => {
    const { service, changes } = harness({}, 1_000_000);
    const file = await xlsx(GRANT_HEADER, [
      [A.memLoginId, 1000, '내용', '사유', ''],
      [B.memLoginId, 500, '내용', '사유', ''],
    ]);
    await service.uploadGrant(19, file, 5, 'COMPENSATION', ctx);
    const c = changes();
    expect(c.corpBalanceBefore).toBe(1_000_000);
    expect(c.corpBalanceBefore - c.corpBalanceAfter).toBe(1500);
  });

  it('충전요청 승인은 기업 후 − 전이 요청 금액이다', async () => {
    const { service, changes } = harness({}, 1_000_000);
    await service.processChargeRequest(
      19,
      { mcrId: 77, action: 'APPROVE', reasonCode: 'CORP_CONTRACT' } as any,
      ctx,
    );
    const c = changes();
    expect(c.mcrId).toBe(77);
    expect(c.corpBalanceBefore).toBe(1_000_000);
    expect(c.corpBalanceAfter - c.corpBalanceBefore).toBe(50_000);
  });
});

describe('memberMoneyChanges — 접기 규칙', () => {
  it('쌍이 한 회원뿐이면 첫 전·마지막 후다', () => {
    expect(
      memberMoneyChanges([
        { memId: 1, before: 100, after: 110 },
        { memId: 1, before: 110, after: 130 },
      ]),
    ).toEqual({ memberMoneyBefore: 100, memberMoneyAfter: 130 });
  });

  it('여러 회원이면 회원별로 접은 값의 합이다', () => {
    expect(
      memberMoneyChanges([
        { memId: 1, before: 100, after: 110 },
        { memId: 2, before: 50, after: 40 },
        { memId: 1, before: 110, after: 130 },
      ]),
    ).toEqual({ memberMoneySumBefore: 150, memberMoneySumAfter: 170 });
  });

  //* 로컬 리뷰 8 — 대상이 없으면 합계 칸을 만들지 않는다. `0 → 0` 은 '움직이지 않았다' 로
  //*  읽히지만 실제는 '잴 대상이 없었다' 이고, 둘은 다른 관측이다.
  it('대상이 없으면 잔액 칸을 아예 싣지 않는다', () => {
    expect(memberMoneyChanges([])).toEqual({});
  });
});
