import { plainToInstance } from 'class-transformer';
import { MoneyAdminController } from './money-admin.controller';
import { MoneyChargeRequestEntity } from './money-charge-request.entity';
import { MoneyCorpEntity } from './money-corp.entity';
import { MoneyIssueEntity } from './money-issue.entity';
import { MoneyPaymentCancelEntity } from './money-payment.entity';
import { MoneyRefundEntity } from './money-refund.entity';
import { processedByCell, processedByOnly } from './money.repository';

/**
 * M9 D4 · D4a — **처리 계정은 감사 열람 자격이 있는 요청자에게만 내려간다.**
 *
 * 세 층 중 이 파일이 두 층을 단언한다. 하나라도 빠지면 게이트 `#4` 의 뮤턴트가 산다:
 *  - 배선(컨트롤러) — 요청자의 자격이 실제로 서비스까지 내려가는가.
 *  - 조립(`processedByCell`) — 자격이 없을 때 칸이 실제로 비는가.
 *
 * **판정 층은 여기 없다.** hardening M6a 가 감사 열람을 환경변수 허용목록에서
 * 최고관리자 판정으로 옮겼고, 그 층의 단언은 `super-admin.service.spec.ts` 가 진다 — 미지정
 * admin · corp · sales · 요청자 부재 · 조회 실패(fail-closed)까지. 여기서 판정을 한 벌 더
 * 복제하면 두 spec 이 조용히 갈린다.
 */
const SUPER = { role: 'admin', admId: 1 } as never;
const PLAIN = { role: 'admin', admId: 20 } as never;

describe('컨트롤러 배선 — 요청자의 자격이 조립 지점까지 내려간다', () => {
  //* 판정은 최고관리자 한 자리다 — 지정된 계정 하나만 참이다(선례 `admin-super.spec.ts`).
  const SUPER_ADM_ID = 1;

  const build = () => {
    const calls: Record<string, unknown[]> = {};
    const svc = {
      getIssueList: jest.fn(
        async (...a: unknown[]) => ((calls.issue = a), { count: 0, list: [] }),
      ),
      getCorpHistory: jest.fn(
        async (...a: unknown[]) => (
          (calls.corp = a), { count: 0, list: [], summary: {} }
        ),
      ),
      getChargeRequestList: jest.fn(
        async (...a: unknown[]) => ((calls.charge = a), { count: 0, list: [] }),
      ),
      getPaymentDetail: jest.fn(
        async (...a: unknown[]) => (
          (calls.detail = a), { payment: {}, cancels: [] }
        ),
      ),
      getPaymentUsage: jest.fn(
        async (...a: unknown[]) => ((calls.usage = a), {}),
      ),
      getRefundList: jest.fn(
        async (...a: unknown[]) => ((calls.refund = a), { count: 0, list: [] }),
      ),
    };
    const superAdmin = {
      isSuperAdmin: jest.fn(
        async (a: any) => Number(a?.admId) === SUPER_ADM_ID,
      ),
    };
    return {
      ctrl: new MoneyAdminController(svc as never, superAdmin as never),
      calls,
    };
  };

  const req = (admin: unknown) => ({ admin }) as never;

  it.each([
    ['최고관리자', SUPER, true],
    ['일반 admin', PLAIN, false],
  ])(
    '%s 요청자의 자격이 여섯 목록·상세에 그대로 간다',
    async (_l, admin, want) => {
      const { ctrl, calls } = build();
      await ctrl.getIssueList(req(admin), {} as never);
      await ctrl.getCorpHistory(req(admin), 1, {} as never);
      await ctrl.getChargeRequestList(req(admin), {} as never);
      await ctrl.getPaymentDetail(req(admin), 1);
      await ctrl.getPaymentUsage(req(admin), 1);
      await ctrl.getRefundList(req(admin), {} as never);

      expect(calls.issue[1]).toBe(want);
      expect(calls.corp[2]).toBe(want);
      expect(calls.charge[2]).toBe(want);
      expect(calls.detail[1]).toBe(want);
      expect(calls.usage[1]).toBe(want);
      expect(calls.refund[1]).toBe(want);
    },
  );
});

describe('processedByCell — 자격이 없으면 칸이 빈다', () => {
  //* `drizzle/schema` 는 jest 에서 스텁이라 `tnAdmin.admLoginId` 가 `undefined` 다. 그래서 값의
  //*  '실체' 가 아니라 **원본 칸을 가리키는가**로 단언한다 — 스텁 유무와 무관하게 성립한다.
  const col = { name: 'adm_id' };
  const open = () => processedByCell(col, true);
  const shut = () => processedByCell(col, false);

  it('자격이 있으면 처리 계정 칸이 원본 칸을 가리킨다', () => {
    expect(open().admId).toBe(col);
    expect(JSON.stringify(open())).toContain('adm_id');
  });

  /**
   * `admId` 만 남겨도 계정을 특정하는 식별자이고, `deleted` 는 '처리자가 있었는데 삭제됐다' 를
   * 말해 계정의 존재 자체를 흘린다 — 세 칸을 함께 비운다.
   */
  it('자격이 없으면 세 칸 어디에도 원본 칸이 없다', () => {
    expect(shut().admId).not.toBe(col);
    expect(JSON.stringify(shut())).not.toContain('adm_id');
  });

  it('세 칸의 이름은 자격과 무관하게 같다 — 화면 형상이 바뀌지 않는다', () => {
    expect(Object.keys(shut()).sort()).toEqual(Object.keys(open()).sort());
  });
});

describe('processedByOnly — 형제 키도 자격에 걸린다', () => {
  //* 로컬 코드리뷰 CRITICAL — `processedByCell` 이 세 칸을 비워도 같은 select 의 형제 키가 같은
  //*  사실을 다른 이름으로 실으면 가림이 그 자리에서 우회된다.
  const col = { name: 'adm_name' };

  it('자격이 있으면 원본 칸을 그대로 돌려준다', () => {
    expect(processedByOnly(col, true)).toBe(col);
  });

  it('자격이 없으면 원본 칸이 값에 없다', () => {
    const shut = processedByOnly(col, false);
    expect(shut).not.toBe(col);
    expect(JSON.stringify(shut)).not.toContain('adm_name');
  });
});

/**
 * **응답 계약**으로도 한 번 더 막는다. 질의에서 형제 키를 지웠어도 DTO 가 그 이름을 `@Expose()`
 * 로 들고 있으면 다른 질의·다음 수정이 그 자리를 조용히 되살린다 — `excludeExtraneousValues` 가
 * 선언 없는 키를 떨어뜨리는 성질을 오라클로 쓴다.
 */
describe('응답 계약 — 처리 계정을 다른 이름으로 싣는 형제 키가 없다', () => {
  const LEAK = {
    admId: 19,
    approvedAdmId: 19,
    approvedAdmLoginId: 'rsteam',
    admName: '홍길동',
  };

  it.each([
    ['적립 목록·상세', MoneyIssueEntity],
    ['기업머니 이력', MoneyCorpEntity],
    ['환불 목록·상세', MoneyRefundEntity],
    ['결제 취소 내역', MoneyPaymentCancelEntity],
    ['충전요청 목록', MoneyChargeRequestEntity],
  ])('%s 은 형제 키를 내리지 않는다', (_label, Entity) => {
    const out = plainToInstance(Entity as never, LEAK, {
      excludeExtraneousValues: true,
    }) as Record<string, unknown>;
    for (const k of Object.keys(LEAK)) expect(out[k]).toBeUndefined();
  });

  /** 승인자 이름 칸 자체는 남는다 — 가림은 질의가 지고 DTO 는 형태를 지킨다(M7 D1). */
  it('충전요청의 승인자 이름 칸은 남아 있다', () => {
    const out = plainToInstance(
      MoneyChargeRequestEntity,
      { approvedAdmName: '홍길동' },
      { excludeExtraneousValues: true },
    );
    expect(out.approvedAdmName).toBe('홍길동');
  });

  /** 처리 계정 덩어리는 그대로다 — 형제 키를 지운 것이 목적물을 지운 것이 아니다. */
  it('processedBy 덩어리는 그대로 내려간다', () => {
    const out = plainToInstance(
      MoneyCorpEntity,
      { processedBy: { loginId: 'rsteam', admId: 19, deleted: 0 } },
      { excludeExtraneousValues: true },
    );
    expect(out.processedBy).toMatchObject({ loginId: 'rsteam', admId: 19 });
  });
});
