import { Logger } from '@nestjs/common';
import { inspect } from 'node:util';
import { TossException } from './toss.exception';
import { TossService } from './toss.service';

const SECRET_KEY = 'test_sk_unit_secret';
const BILLING_KEY = 'bk-unit-1234';
const BASE_URL = 'https://toss.example.test';
const ENDPOINT = '/v1/billing/';

//* 설정값 — TossService 가 생성자와 호출 때 읽는 세 키.
const CONFIG: Record<string, string> = {
  'toss.secretKey': SECRET_KEY,
  'toss.baseUrl': BASE_URL,
  'toss.payBillingEndPoint': ENDPOINT,
};

const setup = () => {
  const web = {
    post: jest.fn().mockResolvedValue({}),
    delete: jest.fn().mockResolvedValue(undefined),
  };
  const config = { get: jest.fn((key: string) => CONFIG[key]) };
  const service = new TossService(config as any, web as any);
  return { service, web };
};

//* Axios 오류 형상 — 응답이 있으면 status · data, 없으면 네트워크 오류. config 에 비밀이 실린다.
const axiosLikeError = (status?: number, data?: unknown) =>
  Object.assign(new Error('Request failed'), {
    config: {
      url: BASE_URL + ENDPOINT + BILLING_KEY,
      auth: { username: SECRET_KEY, password: '' },
    },
    ...(status === undefined ? {} : { response: { status, data } }),
  });

describe('TossService.deleteBillingKey', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('빌링키 폐기: DELETE 를 결제 엔드포인트 + 빌링키 경로로 시크릿 키 기본 인증과 함께 보낸다', async () => {
    const { service, web } = setup();

    await service.deleteBillingKey(BILLING_KEY);

    expect(web.delete).toHaveBeenCalledTimes(1);
    expect(web.delete).toHaveBeenCalledWith(BASE_URL + ENDPOINT + BILLING_KEY, {
      auth: { username: SECRET_KEY, password: '' },
      timeout: 30 * 1000,
      maxRedirects: 0,
    });
    expect(web.post).not.toHaveBeenCalled();
  });

  it('빌링키 폐기: 200 이면 폐기됨으로 답한다', async () => {
    const { service } = setup();

    expect(await service.deleteBillingKey(BILLING_KEY)).toBe('deleted');
  });

  it('빌링키 폐기: 404 NOT_FOUND_BILLING 이면 이미 없음으로 답한다', async () => {
    const { service, web } = setup();
    web.delete.mockRejectedValue(
      axiosLikeError(404, {
        code: 'NOT_FOUND_BILLING',
        message: '존재하지 않는 빌링 결제 인증 정보 입니다.',
      }),
    );

    expect(await service.deleteBillingKey(BILLING_KEY)).toBe('already-gone');
  });

  it('빌링키 폐기: 400 ALREADY_REMOVED_BILLING_KEY(이미 폐기한 키) 면 이미 없음으로 답한다', async () => {
    const { service, web } = setup();
    web.delete.mockRejectedValue(
      axiosLikeError(400, {
        code: 'ALREADY_REMOVED_BILLING_KEY',
        message: '이미 삭제된 빌링키입니다.',
      }),
    );

    expect(await service.deleteBillingKey(BILLING_KEY)).toBe('already-gone');
  });

  it('빌링키 폐기: 다른 오류 코드(404 NOT_FOUND 포함) · 5xx · 응답 없음은 던진다', async () => {
    const { service, web } = setup();
    const logSpy = jest
      .spyOn(Logger.prototype, 'error')
      .mockImplementation(() => undefined);
    const failures = [
      axiosLikeError(404, {
        code: 'NOT_FOUND',
        message: '존재하지 않는 정보 입니다.',
      }),
      axiosLikeError(404, {}),
      axiosLikeError(400, {
        code: 'INVALID_BILL_KEY_REQUEST',
        message: '잘못된 요청',
      }),
      axiosLikeError(400, {
        code: 'NOT_FOUND_BILLING',
        message: '404 가 아니면 이미 없음이 아니다',
      }),
      axiosLikeError(404, {
        code: 'ALREADY_REMOVED_BILLING_KEY',
        message: '400 이 아니면 이미 없음이 아니다',
      }),
      axiosLikeError(500, {
        code: 'FAILED_INTERNAL_SYSTEM_PROCESSING',
        message: '내부 오류',
      }),
      //* 리다이렉트는 따라가지 않으므로(maxRedirects 0) 3xx 도 실패다.
      axiosLikeError(302, {}),
      //* 응답 본문의 code 는 로그 줄을 위조할 수 있다 — 줄바꿈 · 길이가 그대로 남지 않아야 한다.
      axiosLikeError(400, { code: `X\nFORGED ${'A'.repeat(200)}` }),
      axiosLikeError(),
    ];

    for (const failure of failures) {
      web.delete.mockRejectedValueOnce(failure);
      await expect(
        service.deleteBillingKey(BILLING_KEY),
      ).rejects.toBeInstanceOf(TossException);
    }

    //* 실패 로그에는 상태 · 코드만 남는다 — 오류 객체째 남기면 config 의 시크릿 키 · 빌링키 URL 이 샌다.
    expect(logSpy).toHaveBeenCalledTimes(failures.length);
    const logged = logSpy.mock.calls
      .map((args) => args.map((arg) => inspect(arg, { depth: 8 })).join(' '))
      .join('\n');
    expect(logged).not.toContain(SECRET_KEY);
    expect(logged).not.toContain(BILLING_KEY);
    expect(logged).not.toContain('\nFORGED');
    expect(logged).not.toContain('A'.repeat(100));
  });

  it('빌링키 폐기: 2xx 라도 본문이 비어 있지 않은 문자열이면 폐기됨으로 읽지 않고 던진다', async () => {
    const { service, web } = setup();
    jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
    //* 성공 대역은 인터셉터가 푼 본문이다 — 빈 본문(토스 성공)은 '' 다.
    web.delete.mockResolvedValueOnce('');
    expect(await service.deleteBillingKey(BILLING_KEY)).toBe('deleted');

    web.delete.mockResolvedValueOnce('<html>login</html>');
    await expect(service.deleteBillingKey(BILLING_KEY)).rejects.toBeInstanceOf(
      TossException,
    );
  });

  it('빌링키 폐기: 빈 빌링키는 토스를 부르지 않고 이미 없음으로 답한다', async () => {
    const { service, web } = setup();

    expect(await service.deleteBillingKey('')).toBe('already-gone');
    expect(web.delete).not.toHaveBeenCalled();
    expect(web.post).not.toHaveBeenCalled();
  });
});
