import { AlertAdminController } from './alert-admin.controller';
import { maskWebhookUrl } from './alert-url';

/**
 * §D7 1 툼스톤의 **실패 분기**만 검증한다.
 *
 * 초안은 무효화 실패 시 변경을 거부했는데, 그러면 Redis 장애 동안 `alert_enabled=false`
 * 저장이 막혀 kill switch 가 잠긴다(UI16 위반). 우회로가 없다는 것이 핵심이라
 * 이 세 테스트가 깨지면 그 데드락이 돌아온 것이다.
 */
describe('AlertAdminController — 폴백 스냅샷 무효화(§D7 1)', () => {
  const build = (invalidateSnapshot: jest.Mock, trace: string[] = []) => {
    const repo = {
      insertChannel: jest.fn(async () => void trace.push('insertChannel')),
      deleteChannel: jest.fn(async () => void trace.push('deleteChannel')),
    };
    const settings = {
      patch: jest.fn(async () => {
        trace.push('patch');
        return {};
      }),
      setRuleEnabled: jest.fn(),
    };
    const alert = { settingCatalog: [], ruleCatalog: [], allowedSuffixes: [] };
    const controller = new AlertAdminController(
      alert as any,
      repo as any,
      settings as any,
      { invalidateSnapshot } as any,
      {} as any, //* AlertMonitorService — 이 describe 는 GET /monitor 를 부르지 않는다
    );
    return { controller, repo, settings, trace };
  };

  const redisDown = () =>
    jest.fn(async () => {
      throw new Error('Reached the max retries per request limit');
    });

  it('무효화 실패가 alert_enabled=false 저장을 막지 않는다 (kill switch 잠김 방지, UI16)', async () => {
    const invalidate = redisDown();
    const { controller, settings } = build(invalidate);

    await expect(
      controller.patchConfig({ values: { alert_enabled: false } } as any),
    ).resolves.toEqual({ updated: true, applied: {} });

    expect(invalidate).toHaveBeenCalledTimes(1); //* 시도는 한다
    expect(settings.patch).toHaveBeenCalledTimes(1); //* 그리고 거부하지 않는다
  });

  it('무효화 실패가 채널 삭제도 막지 않는다', async () => {
    const invalidate = redisDown();
    const { controller, repo } = build(invalidate);

    await expect(controller.deleteChannel(7)).resolves.toEqual({ deleted: true });
    expect(repo.deleteChannel).toHaveBeenCalledWith(7);
  });

  it('무효화는 DB 쓰기보다 **먼저** 시도된다 (창을 만들지 않는다)', async () => {
    const trace: string[] = [];
    const invalidate = jest.fn(async () => void trace.push('invalidate'));
    const { controller } = build(invalidate, trace);

    await controller.patchConfig({ values: { alert_dedupe_ms: 1000 } } as any);
    await controller.deleteChannel(1);

    expect(trace).toEqual(['invalidate', 'patch', 'invalidate', 'deleteChannel']);
  });
});

//=================== M3 — 스누즈 · 자기감시 (§D7 · §D8) ===================//

describe('AlertAdminController — 스누즈(M3 §D7)', () => {
  const build = (over: { ruleCatalog?: any[] } = {}) => {
    const invalidateSnapshot = jest.fn(async () => undefined);
    const settings = { setSnooze: jest.fn(async () => undefined) };
    const alert = {
      settingCatalog: [],
      ruleCatalog: over.ruleCatalog ?? [{ id: 'api-error-rate', name: 'API', severity: 'critical' }],
      allowedSuffixes: [],
    };
    const controller = new AlertAdminController(
      alert as any,
      {} as any,
      settings as any,
      { invalidateSnapshot } as any,
      {} as any, //* AlertMonitorService — 스누즈 경로는 monitor 를 만지지 않는다
    );
    return { controller, settings, invalidateSnapshot };
  };
  const req = { admin: { admId: 42, role: 'admin' } } as any;

  it('상한 24시간을 넘기면 400 이다 — 무기한 침묵 경로를 만들지 않는다', async () => {
    const { controller } = build();
    await expect(controller.snooze({ minutes: 1441 } as any, req)).rejects.toThrow('1440분');
    await expect(controller.snooze({ minutes: -1 } as any, req)).rejects.toThrow();
  });

  it('알 수 없는 규칙 id 는 400 이다', async () => {
    const { controller } = build();
    await expect(controller.snooze({ minutes: 10, ruleId: 'nope' } as any, req)).rejects.toThrow(
      '알 수 없는 규칙 id',
    );
  });

  it('상대시간을 서버가 절대시각으로 바꾸고 admId 를 감사 필드로 남긴다', async () => {
    const { controller, settings } = build();
    const before = Date.now();

    const res = await controller.snooze({ minutes: 30 } as any, req);

    expect(res.until).toBeGreaterThanOrEqual(before + 30 * 60_000);
    expect(settings.setSnooze).toHaveBeenCalledWith(null, res.until, 42);
  });

  it('해제(minutes:0)는 until 0 이다', async () => {
    const { controller, settings } = build();
    const res = await controller.snooze({ minutes: 0 } as any, req);
    expect(res.until).toBe(0);
    expect(settings.setSnooze).toHaveBeenCalledWith(null, 0, 42);
  });

  it('변경 **전에** 스냅샷을 무효화한다 — 스누즈 직후 DB 사망 시 우회를 막는다 (R6-F2)', async () => {
    const trace: string[] = [];
    const invalidateSnapshot = jest.fn(async () => void trace.push('invalidate'));
    const settings = { setSnooze: jest.fn(async () => void trace.push('setSnooze')) };
    const controller = new AlertAdminController(
      { ruleCatalog: [], settingCatalog: [], allowedSuffixes: [] } as any,
      {} as any,
      settings as any,
      { invalidateSnapshot } as any,
      {} as any, //* AlertMonitorService — 순서 검증만 하므로 쓰이지 않는다
    );

    await controller.snooze({ minutes: 10 } as any, req);

    expect(trace).toEqual(['invalidate', 'setSnooze']);
  });
});

//* `GET /monitor` 의 조립 검증은 `alert-monitor.service.spec.ts` 로 옮겼다 —
//*  컨트롤러는 위임만 하므로 여기서 다시 검증할 표면이 없다.

/**
 * `testTargets` 런타임 계약 (§D10 세 번째 층).
 *
 * `MaskedUrl` 브랜드는 **JSON 직렬화에서 지워진다** — 타입은 응답에 실린 **값**을 증명하지 못한다.
 * 그리고 "원문 URL 을 포함하지 않는다" 만 요구하면 `raw.replace(/^https:...//, '')` 같은 변형이
 * 경로·키·토큰을 그대로 남긴 채 통과한다(R17 F2 · Implement-Codex R1 F1).
 *
 * 그래서 **`maskWebhookUrl(raw)` 와의 정확 일치**와 고정 안전 형태를 함께 단언한다.
 * `maskWebhookUrl` 의 출력은 셋뿐이다 — `''` · `'***'` · `{protocol}//{host}/***`.
 */
describe('AlertAdminController — testTargets 런타임 계약(§D10)', () => {
  const RAW =
    'https://chat.googleapis.com/v1/spaces/AAAQSECRETSPACE/messages?key=SECRETKEY&token=SECRETTOKEN';

  const build = () => {
    const alert = {
      ruleCatalog: [
        {
          id: 'treat-monitor',
          name: '진료 모니터링',
          severity: 'critical',
          noise: { group: true },
          settings: [],
        },
      ],
      testTargets: jest.fn(async () => ({
        hints: ['octover'],
        legacyGlobal: { url: RAW, types: ['provider'] },
      })),
    };
    const controller = new AlertAdminController(
      alert as any,
      {} as any,
      {} as any,
      {} as any,
      {} as any,
    );
    return { controller };
  };

  it('testTargets 계약: legacyGlobal 원문 미노출 — 직렬화된 값이 마스킹 출력과 정확히 같다', async () => {
    const { controller } = build();
    //* 브랜드가 지워지는 지점을 그대로 재현한다 — 이 왕복이 이 계약의 요점이다.
    const wire = JSON.parse(JSON.stringify(await controller.getRules()));
    const got = wire.list[0].testTargets.legacyGlobal.urlMasked;

    expect(got).toBe(maskWebhookUrl(RAW));
    expect(got).toBe('https://chat.googleapis.com/***');
  });

  it('testTargets 계약: legacyGlobal 원문 미노출 — 경로·키·토큰 조각이 응답 전체에 없다', async () => {
    const { controller } = build();
    const wire = JSON.stringify(await controller.getRules());

    for (const fragment of [
      RAW,
      'SECRETKEY',
      'SECRETTOKEN',
      'AAAQSECRETSPACE',
      'spaces',
      'messages',
      'key=',
      'token=',
    ])
      expect(wire).not.toContain(fragment);
  });
});
