import { InternalServerErrorException } from '@nestjs/common';
import { AuditActorRole, AuditContext } from '../audit/audit.entity';
import { MoneyService } from './money.service';

/**
 * 금전 표(`th_money_corp`)에 남는 **행위자**의 계약.
 *
 * 두 결함이 같은 자리에 있었다. (a) `createCorpMoney` 가 `mcpActorType: 'ADMIN'` 을 하드코딩해
 * 영업대리점(sales)의 충전·차감이 **서비스 관리자 행위로 오표기**됐고 — 이 라우트는 클래스
 * 가드가 `@Roles('admin','sales')` 라 sales 가 실제로 도달한다 — (b) 리터럴만 지우면 폴백이
 * 형태를 못 알아본 요청까지 `ADMIN` 으로 떨어뜨려 오표기가 그대로 남는다.
 *
 * 옳은 자리는 **거부**다. `'UNKNOWN'` 을 금전 컬럼에 쓰면 `mcp_actor_type` 을 읽는 관리자 API
 * 응답까지 값 도메인이 넓어지고, `ADMIN` 으로 떨어뜨리면 거짓 신원이 남는다.
 */
const TX = { __tx: true } as any;

function harness(over: Record<string, unknown> = {}) {
  const createCorpHistory = jest.fn();
  const updateAgencyMoney = jest.fn();
  const base: Record<string, unknown> = {
    runInTransaction: async (fn: (tx: unknown) => unknown) => fn(TX),
    getAgencyByIdForUpdate: async () => ({ agcId: 5, agcMoney: 1_000_000 }),
    createCorpHistory,
    updateAgencyMoney,
    ...over,
  };
  //* 나머지 저장소 메서드는 부수효과만 있고 이 계약이 보는 값에 관여하지 않는다.
  const repository = new Proxy({} as any, {
    get: (_t, k: string) => (k in base ? base[k] : async () => undefined),
  });
  const audit = { recordMoney: jest.fn(), recordRequest: jest.fn(), recordSystem: jest.fn() };
  Object.assign(audit, { appendMoney: jest.fn() });
  const service = new MoneyService(repository, {} as any, {} as any, {} as any, audit as any);
  return { service, createCorpHistory, audit };
}

const ctxOf = (role: AuditActorRole, refId: number | null = null): AuditContext => ({
  actor: { role, refId, refLabel: null, orgPath: null, personId: null },
  requestId: 'req-a',
  ip: '192.0.2.10',
  userAgent: 'test-agent',
});

const dto = () => ({ agcId: 5, mcpAmount: 10_000, mcpTitle: 't', mcpReason: null, reasonCode: 'CORRECTION' }) as any;

describe('createCorpMoney — 행위자를 요청 맥락에서 정한다', () => {
  it.each([
    ['admin', 'ADMIN' as const, 19],
    ['sales', 'SALES' as const, 7],
  ])('%s 토큰은 %s 로 남는다 — 하드코딩된 ADMIN 이 아니다', async (_l, role, refId) => {
    const { service, createCorpHistory } = harness();
    await service.createCorpMoney(refId, dto(), ctxOf(role, refId));
    expect(createCorpHistory).toHaveBeenCalledTimes(1);
    expect(createCorpHistory.mock.calls[0][0]).toMatchObject({
      mcpActorType: role,
      mcpActorMemId: null,
    });
  });

  /**
   * 이 케이스가 오표기의 직접 항체다 — sales 가 `ADMIN` 으로 남으면 여기서 죽는다.
   */
  it('sales 충전은 ADMIN 으로 남지 않는다', async () => {
    const { service, createCorpHistory } = harness();
    await service.createCorpMoney(7, dto(), ctxOf('SALES', 7));
    expect(createCorpHistory.mock.calls[0][0].mcpActorType).not.toBe('ADMIN');
  });

  it('감사 줄의 행위자와 금전 표의 행위자가 같은 요청에서 갈리지 않는다', async () => {
    const { service, createCorpHistory, audit } = harness();
    await service.createCorpMoney(7, dto(), ctxOf('SALES', 7));
    expect(createCorpHistory.mock.calls[0][0].mcpActorType).toBe('SALES');
    expect(audit.recordMoney.mock.calls[0][0].actor.role).toBe('SALES');
  });
});

describe('getMoneyActor — 형태를 못 알아본 요청은 거부한다', () => {
  it.each([
    ['UNKNOWN', 'UNKNOWN' as const],
    ['SYSTEM', 'SYSTEM' as const],
    ['스코프 없는 CORP', 'CORP' as const],
  ])('%s 행위자의 충전 요청은 거부된다', async (_l, role) => {
    const { service } = harness();
    await expect(service.createCorpMoney(null, dto(), ctxOf(role))).rejects.toBeInstanceOf(
      InternalServerErrorException,
    );
  });

  it('거부된 요청은 금전 표에 한 행도 쓰지 않는다', async () => {
    const { service, createCorpHistory } = harness();
    await expect(service.createCorpMoney(null, dto(), ctxOf('UNKNOWN'))).rejects.toThrow();
    expect(createCorpHistory).not.toHaveBeenCalled();
  });

  it("금전 컬럼의 값 도메인을 넓히지 않는다 — 'UNKNOWN' 을 쓰지 않는다", async () => {
    const { service, createCorpHistory } = harness();
    await expect(service.createCorpMoney(null, dto(), ctxOf('UNKNOWN'))).rejects.toThrow();
    const written = createCorpHistory.mock.calls.map((c) => c[0]?.mcpActorType);
    expect(written).not.toContain('UNKNOWN');
  });

  /**
   * `AllExceptionFilter` 가 `exception.getResponse()` 를 클라이언트에 그대로 되돌린다 —
   * 본문에는 **고정 불투명 코드**만 있어야 한다.
   */
  it('거부 본문은 고정 불투명 코드만 싣는다', async () => {
    const { service } = harness();
    const err = await service
      .createCorpMoney(null, dto(), ctxOf('UNKNOWN'))
      .catch((e: InternalServerErrorException) => e);
    const body = (err as InternalServerErrorException).getResponse() as Record<string, unknown>;
    expect(body.message).toBe('E_MONEY_ACTOR_UNRESOLVED');
    //* `error` 는 Nest 가 붙이는 상태코드 이름('Internal Server Error')이라 내부 사정이 아니다.
    //*  그 셋 **밖**의 키가 생기면 이 케이스가 죽는다 — 본문에 사정을 실은 것이기 때문이다.
    expect(Object.keys(body).sort()).toEqual(['error', 'message', 'statusCode']);
    expect(String(body.error)).toBe('Internal Server Error');
  });

  it('ctx 가 아예 없는 내부 호출은 예전대로 ADMIN 이다 — 이 거부는 HTTP 경로의 그물이다', async () => {
    const { service, createCorpHistory } = harness();
    //* 금전 라우트 여섯 곳은 ctx 를 필수 인자로 받으므로 이 폴백에 도달하지 않는다.
    await service.createCorpMoney(19, dto(), ctxOf('ADMIN', 19));
    expect(createCorpHistory.mock.calls[0][0].mcpActorType).toBe('ADMIN');
  });
});
