/**
 * 이관 계획 spec (M5 Task 3 · 검사 #6 의 scripts 제목). DB 는 열지 않는다.
 * 순수 함수 (`alert/legacy-destination.ts`) 와 CLI 의 순수 판정 (`applyRefusal` · `describeError`) 만 본다.
 * 기대값은 손으로 계산해 적는다 — 구현을 다시 적지 않는다.
 */
import { isAllowedWebhookUrl, parseAllowSuffixEnv } from '../apps/api/src/modules/alert/alert-url';
import {
  type MigrationInput,
  destinationHash,
  planDigest,
  planLegacyMigration,
  renderPlan,
} from '../apps/api/src/modules/alert/legacy-destination';
import { applyRefusal, describeError } from './legacy-destination-migrate';

const FULL = ['success_rate', 'provider', 'queue'];
const allowed = parseAllowSuffixEnv(undefined).allowed; //* 기본 허용: 구글챗 · 슬랙
const isAllowed = (u: string) => isAllowedWebhookUrl(u, allowed);
const A = 'https://hooks.slack.com/services/A/ops';
const G = 'https://hooks.slack.com/services/G/global';
const V = 'https://hooks.slack.com/services/V/inhouse';
const h = destinationHash;

const ch = (alcId: number, alcName: string, alcUrl: string, alcTypes: string | null, alcEnabled = 1) => ({
  alcId,
  alcName,
  alcUrl,
  alcTypes,
  alcEnabled,
});

const base = (over: Partial<MigrationInput> = {}): MigrationInput => ({
  global: { url: '', types: FULL },
  vendors: [],
  channels: [ch(1, 'ops', A, null)],
  fullKinds: FULL,
  isAllowed,
  ...over,
});

const SECRET_PARTS = [
  'SECRETTOKEN',
  'PATHPART',
  'QUERYSECRET',
  'user:pw',
  'hooks.slack.com',
  'chat.googleapis.com',
  'SPACEID',
  'KEYSECRET',
];

describe('이관 계획 (검사 #6)', () => {
  it('전역 레거시는 규칙 단위 행으로, 벤더 레거시는 힌트 단위 행으로 옮긴다', () => {
    const plan = planLegacyMigration(
      base({ global: { url: G, types: FULL }, vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }] }),
    );
    expect(plan.rows).toEqual([
      expect.objectContaining({ name: 'legacy-global', types: ['treat-monitor'], hint: null, action: 'create' }),
      expect.objectContaining({ name: 'legacy-vendor:inhouse', types: ['inhouse'], hint: 'inhouse', action: 'create' }),
    ]);
  });

  it('이관 계획은 규칙·힌트별 목적지 쌍을 보존한다', () => {
    //* 손계산: 기존 ops(A, 전체 종류) · 전역 G(전체 종류) · 벤더 inhouse→V · 벤더 octover 는 URL 없음.
    //*  힌트는 없음 · inhouse · octover · ops(채널 이름) 네 가지다.
    //*  - 없음: 일반 채널 ops 와 전역 행 (treat-monitor 만) → treat-monitor 는 [A, G], treat-vendor 는 [A]
    //*  - inhouse: 힌트 행이 잡는다 → 두 규칙 모두 [V]
    //*  - octover: 힌트 채널이 없다 → 일반 채널과 같다
    //*  - ops: 채널 이름이 힌트라 ops 가 잡는다 → 두 규칙 모두 [A]
    const plan = planLegacyMigration(
      base({ global: { url: G, types: FULL }, vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }, { tprKey: 'octover', tprWebhookUrl: '' }] }),
    );
    const expected = [
      ['treat-monitor', null, h(A)],
      ['treat-monitor', null, h(G)],
      ['treat-vendor', null, h(A)],
      ['treat-monitor', 'inhouse', h(V)],
      ['treat-vendor', 'inhouse', h(V)],
      ['treat-monitor', 'octover', h(A)],
      ['treat-monitor', 'octover', h(G)],
      ['treat-vendor', 'octover', h(A)],
      ['treat-monitor', 'ops', h(A)],
      ['treat-vendor', 'ops', h(A)],
    ].map((t) => JSON.stringify(t));
    expect(plan.pairs).toEqual([...expected].sort());
    expect(plan.preservation).toBe(0);
  });

  it('같은 URL 이라도 비활성 · 다른 규칙 행은 재사용하지 않는다', () => {
    const inactive = planLegacyMigration(
      base({ global: { url: G, types: ['treat-monitor'] }, channels: [ch(1, 'old', G, '["treat-monitor"]', 0)] }),
    );
    expect(inactive.rows[0]).toMatchObject({ action: 'create', mismatch: true });

    const otherRule = planLegacyMigration(
      base({ global: { url: G, types: ['treat-monitor'] }, channels: [ch(1, 'other', G, '["treat-vendor"]')] }),
    );
    expect(otherRule.rows[0]).toMatchObject({ action: 'create', mismatch: true });

    const reusable = planLegacyMigration(
      base({ global: { url: G, types: ['treat-monitor'] }, channels: [ch(1, 'g', G, '["treat-monitor"]')] }),
    );
    expect(reusable.rows[0]).toMatchObject({ action: 'reuse', channelId: 1 });
    expect(reusable.mismatch).toBe(0);
  });

  it('재실행은 새 행 0건이다', () => {
    const input = base({ global: { url: G, types: FULL }, vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }] });
    const first = planLegacyMigration(input);
    expect(first.createCount).toBe(2);
    //* 첫 적용이 낸 행을 채널 목록에 넣고 다시 계획한다 (CLI 가 INSERT 한 뒤의 상태와 같다).
    const written = [
      ...input.channels,
      ...first.rows
        .filter((r) => r.action === 'create')
        .map((r) => ({ alcId: r.channelId!, alcName: r.name, alcUrl: r.url, alcTypes: JSON.stringify(r.types), alcEnabled: 1 })),
    ];
    const second = planLegacyMigration({ ...input, channels: written });
    expect(second.createCount).toBe(0);
    expect(second.unmigrated).toBe(0);
    expect(second.digest).toBe(first.digest);
  });

  it('같은 힌트를 구독하는 일반 채널은 우선순위 탈락으로 집계된다', () => {
    const plan = planLegacyMigration(base({ vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }] }));
    const hintCombo = plan.combos.find((c) => c.rule === 'treat-monitor' && c.hint === 'inhouse');
    expect(hintCombo?.displacedChannelIds).toEqual([1]);
    expect(plan.preservation).toBe(0);
  });

  it('전역 레거시의 종류가 기본 집합과 다르면 부분 종류로 집계된다', () => {
    const partial = planLegacyMigration(base({ global: { url: G, types: ['provider'] } }));
    expect(partial.partialKinds).toBe(true);
    const whole = planLegacyMigration(base({ global: { url: G, types: [...FULL] } }));
    expect(whole.partialKinds).toBe(false);
  });

  it('공백 URL 은 옮기지 않고 같은 URL 이 전역과 벤더에 있으면 원천마다 행을 하나씩 만든다', () => {
    const blank = planLegacyMigration(
      base({ global: { url: '   ', types: FULL }, vendors: [{ tprKey: 'inhouse', tprWebhookUrl: '  ' }] }),
    );
    expect(blank.rows).toEqual([]);

    const same = planLegacyMigration(
      base({ global: { url: G, types: FULL }, vendors: [{ tprKey: 'inhouse', tprWebhookUrl: G }] }),
    );
    expect(same.rows.map((r) => [r.name, r.action])).toEqual([
      ['legacy-global', 'create'],
      ['legacy-vendor:inhouse', 'create'],
    ]);
  });

  it('출력 전문과 오류 출력에 URL 원문과 호스트가 없다', () => {
    const secret = 'https://user:pw@hooks.slack.com/services/SECRETTOKEN/PATHPART?key=QUERYSECRET';
    const plan = planLegacyMigration({
      global: { url: secret, types: FULL },
      vendors: [{ tprKey: 'inhouse', tprWebhookUrl: 'https://chat.googleapis.com/v1/spaces/SPACEID/messages?key=KEYSECRET' }],
      channels: [],
      fullKinds: FULL,
      isAllowed: () => true, //* 허용 술어를 열어 두고 출력만 본다
    });
    const out = renderPlan(plan);
    for (const bad of SECRET_PARTS) expect(out).not.toContain(bad);

    const err = Object.assign(new Error(`insert failed ${secret}`), {
      code: 'ER_DUP_ENTRY',
      errno: 1062,
      sql: `INSERT INTO tn_alert_channel VALUES ('${secret}')`,
      sqlMessage: `Duplicate entry '${secret}'`,
    });
    const msg = describeError(err);
    for (const bad of SECRET_PARTS) expect(msg).not.toContain(bad);
    expect(msg).toContain('ER_DUP_ENTRY');
  });

  it('기존 힌트 채널이 같은 힌트를 먼저 잡으면 옛 벤더 URL 은 보존 실패로 집계되고 --apply 는 거부된다', () => {
    const plan = planLegacyMigration(
      base({ channels: [ch(1, 'inhouse', A, '["inhouse"]')], vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }] }),
    );
    expect(plan.preservation).toBe(1);
    expect(applyRefusal(plan, plan.digest)).toMatch(/보존 실패/);
  });

  it('전역 옛 URL 도 힌트 발화에서 밀리면 보존 실패로 집계된다', () => {
    const plan = planLegacyMigration(
      base({
        global: { url: G, types: FULL },
        channels: [ch(1, 'inhouse', A, '["inhouse"]')],
        vendors: [{ tprKey: 'inhouse', tprWebhookUrl: '' }],
      }),
    );
    expect(plan.preservation).toBe(1);
  });

  it('승인된 추가 수신과 우선순위 탈락은 보존 실패가 아니다', () => {
    const plan = planLegacyMigration(base({ vendors: [{ tprKey: 'inhouse', tprWebhookUrl: V }] }));
    expect(plan.preservation).toBe(0);
    expect(plan.combos.some((c) => c.displacedChannelIds.length > 0)).toBe(true);
    expect(plan.combos.some((c) => c.added.length > 0)).toBe(true);
    expect(applyRefusal(plan, plan.digest)).toBeNull();
  });

  it('승인 해시는 같은 계획에 같은 값을, 다른 계획에 다른 값을 낸다', () => {
    const p1 = planLegacyMigration(base({ global: { url: G, types: FULL } }));
    const p1again = planLegacyMigration(base({ global: { url: G, types: FULL } }));
    const p2 = planLegacyMigration(base({ global: { url: V, types: FULL } }));
    expect(planDigest(p1)).toBe(planDigest(p1again));
    expect(planDigest(p1)).not.toBe(planDigest(p2));
    expect(planDigest(p1)).toHaveLength(16);
  });

  it('sql 필드에 URL 이 든 예외도 출력에 URL 이 없다', () => {
    const secret = 'https://hooks.slack.com/services/SECRETTOKEN/PATHPART?key=QUERYSECRET';
    const err = Object.assign(new Error('query failed'), {
      code: 'ER_NO_SUCH_TABLE',
      errno: 1146,
      sql: `UPDATE tn_treat_setting SET tst_value='${secret}'`,
    });
    const msg = describeError(err);
    for (const bad of SECRET_PARTS) expect(msg).not.toContain(bad);
    expect(msg).toContain('1146');
  });
});
