import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import {
  CreateMoneyCorpDto,
  CreateMoneyGrantDto,
  CreateMoneyGrantSingleDto,
  CreateMoneyIssueDto,
  ProcessMoneyChargeRequestDto,
} from './money.dto';

/**
 * 사유 유형 필드의 DTO 계약 — 계획 M8 D1·D5 · Task 3.
 *
 * 판정의 정본은 서비스의 `assertInterventionReason` 이다. 여기서 보는 것은 **전역 파이프 층**이다 —
 * 데코레이터 없는 필드는 `forbidNonWhitelisted` 로 400 이 되고(`apps/api/src/pipes/validation.pipe.ts`),
 * `@IsOptional()` 이 붙으면 빠진 요청이 파이프를 지난다. 파이프와 같은 옵션으로 검증한다.
 *
 * 게이트 `#4` 가 이 파일에 뮤턴트 둘을 건다: 네 DTO 의 `reasonCode` 를 선택으로 바꾼 사본 · 승인도
 * 사유를 면제한 사본.
 *
 * 2026-10 변경: 기업회원 지급 DTO 둘(`CreateMoneyGrantDto`·`CreateMoneyGrantSingleDto`)은 선택이 됐다 —
 * 기업·조직관리자 콘솔이 사유 유형을 보내지 않고 서버가 배분/회수로 정한다. 관리자·영업대리점의 필수는
 * 서비스 판정이 그대로 지킨다(`money-audit-reason.spec.ts` 의 경로별 거절 케이스).
 */
const PIPE = { whitelist: true, forbidNonWhitelisted: true };

async function reasonErrors(cls: new () => object, body: object) {
  const errors = await validate(plainToInstance(cls, body), PIPE);
  return errors.filter((e) => e.property === 'reasonCode');
}

describe.each([
  ['CreateMoneyIssueDto', CreateMoneyIssueDto],
  ['CreateMoneyCorpDto', CreateMoneyCorpDto],
] as [string, new () => object][])('%s — 사유 유형이 필수다', (_name, cls) => {
  it('reasonCode 가 없으면 그 필드에 오류가 난다', async () => {
    expect(await reasonErrors(cls, {})).toHaveLength(1);
  });

  it('reasonCode 가 문자열이 아니면 오류가 난다', async () => {
    expect(await reasonErrors(cls, { reasonCode: 3 })).toHaveLength(1);
  });

  it('reasonCode 가 있으면 그 필드에 오류가 없다', async () => {
    expect(await reasonErrors(cls, { reasonCode: 'CORRECTION' })).toHaveLength(
      0,
    );
  });
});

describe.each([
  ['CreateMoneyGrantDto', CreateMoneyGrantDto],
  ['CreateMoneyGrantSingleDto', CreateMoneyGrantSingleDto],
] as [string, new () => object][])(
  '%s — 기업 콘솔이 생략하므로 DTO 에서는 선택이다',
  (_name, cls) => {
    it('reasonCode 가 없어도 그 필드에 오류가 없다', async () => {
      expect(await reasonErrors(cls, {})).toHaveLength(0);
    });

    it('reasonCode 가 문자열이 아니면 오류가 난다', async () => {
      expect(await reasonErrors(cls, { reasonCode: 3 })).toHaveLength(1);
    });
  },
);

describe('ProcessMoneyChargeRequestDto — 승인만 사유 유형을 요구한다', () => {
  it('승인(APPROVE) 인데 reasonCode 가 없으면 오류가 난다', async () => {
    expect(
      await reasonErrors(ProcessMoneyChargeRequestDto, {
        mcrId: 1,
        action: 'APPROVE',
      }),
    ).toHaveLength(1);
  });

  it('승인(APPROVE) 에 reasonCode 가 있으면 오류가 없다', async () => {
    expect(
      await reasonErrors(ProcessMoneyChargeRequestDto, {
        mcrId: 1,
        action: 'APPROVE',
        reasonCode: 'CORP_CONTRACT',
      }),
    ).toHaveLength(0);
  });

  it('반려(REJECT) 는 reasonCode 가 없어도 오류가 없다', async () => {
    expect(
      await reasonErrors(ProcessMoneyChargeRequestDto, {
        mcrId: 1,
        action: 'REJECT',
        rejectedReason: '중복 요청',
      }),
    ).toHaveLength(0);
  });

  it('반려(REJECT) 가 reasonCode 를 실어 와도 모르는 필드로 거절하지 않는다', async () => {
    const errors = await validate(
      plainToInstance(ProcessMoneyChargeRequestDto, {
        mcrId: 1,
        action: 'REJECT',
        rejectedReason: '중복 요청',
        reasonCode: 'OTHER',
      }),
      PIPE,
    );
    expect(errors).toHaveLength(0);
  });
});
