import {
  isAllowedWebhookUrl,
  isBlockedAddress,
  maskWebhookUrl,
  parseAllowSuffixEnv,
  resolveAllowedAddresses,
  sanitizeAllowSuffixes,
} from './alert-url';

const ALLOW = ['chat.googleapis.com', 'hooks.slack.com'];
const pub = [{ address: '142.250.207.106', family: 4 }];

describe('alert-url — 웹훅 목적지 검증 (D9)', () => {
  describe('이름 축 (조건 1~5)', () => {
    it('허용 호스트의 https 는 통과한다', () => {
      expect(isAllowedWebhookUrl('https://chat.googleapis.com/v1/spaces/A', ALLOW)).toBe(true);
      expect(isAllowedWebhookUrl('https://hooks.slack.com/services/T/B/X', ALLOW)).toBe(true);
    });

    it('http 는 거부한다 (조건 1)', () => {
      expect(isAllowedWebhookUrl('http://chat.googleapis.com/x', ALLOW)).toBe(false);
    });

    it('자격증명이 있으면 거부한다 (조건 2)', () => {
      expect(isAllowedWebhookUrl('https://user:pw@chat.googleapis.com/x', ALLOW)).toBe(false);
    });

    it('비표준 포트는 거부한다 (조건 3)', () => {
      expect(isAllowedWebhookUrl('https://chat.googleapis.com:8080/x', ALLOW)).toBe(false);
      expect(isAllowedWebhookUrl('https://chat.googleapis.com:443/x', ALLOW)).toBe(true);
    });

    it('접미사 매칭은 라벨 경계를 지킨다 (조건 4)', () => {
      expect(isAllowedWebhookUrl('https://notchat.googleapis.com.evil.com/x', ALLOW)).toBe(false);
      expect(isAllowedWebhookUrl('https://evil.com/x', ALLOW)).toBe(false);
      expect(isAllowedWebhookUrl('https://a.b.chat.googleapis.com/x', ALLOW)).toBe(true);
    });

    it('IP 리터럴은 접미사 축을 우회하므로 거부한다 (조건 5)', () => {
      expect(isAllowedWebhookUrl('https://169.254.169.254/x', ALLOW)).toBe(false);
      expect(isAllowedWebhookUrl('https://10.0.0.5/x', ALLOW)).toBe(false);
      expect(isAllowedWebhookUrl('https://[::1]/x', ALLOW)).toBe(false);
    });

    it('정규화 — 대문자 호스트와 후행 점을 처리한다 (Santa R10 A)', () => {
      expect(isAllowedWebhookUrl('https://CHAT.googleapis.com/x', ALLOW)).toBe(true);
      expect(isAllowedWebhookUrl('https://chat.googleapis.com./x', ALLOW)).toBe(true);
    });

    it('파싱 실패는 거부한다', () => {
      expect(isAllowedWebhookUrl('!!bad!!', ALLOW)).toBe(false);
    });

    it('빈 값·공백은 통과한다 — "목적지 없음"이며 검증 대상이 아니다 (회귀 방지)', () => {
      expect(isAllowedWebhookUrl('', ALLOW)).toBe(true);
      expect(isAllowedWebhookUrl('   ', ALLOW)).toBe(true);
    });
  });

  describe('maskWebhookUrl — 출력 형식 고정 (D9-b 변경 여부 판정이 문자열 비교를 한다)', () => {
    it('경로·쿼리를 한 조각도 남기지 않는다', () => {
      expect(maskWebhookUrl('https://chat.googleapis.com/v1/spaces/AAA?key=s')).toBe(
        'https://chat.googleapis.com/***',
      );
    });

    it('빈 값은 그대로 — 비활성 상태를 감추지 않는다', () => {
      expect(maskWebhookUrl('')).toBe('');
      expect(maskWebhookUrl(null)).toBe('');
    });

    it('파싱 실패는 전량 마스킹한다', () => {
      expect(maskWebhookUrl('!!bad!!')).toBe('***');
    });
  });

  describe('허용목록 항목 검사 — 이름 규칙 (D9)', () => {
    it('IP 리터럴·단일 라벨·내부 접미사만 제외되고 나머지는 남는다 (Santa R9)', () => {
      const { allowed, rejected } = sanitizeAllowSuffixes([
        '10.0.0.5',
        'intranet',
        'svc.internal',
        'foo.local',
        'chat.googleapis.com',
      ]);
      expect(allowed).toEqual(['chat.googleapis.com']);
      expect(rejected.map((r) => r.suffix)).toEqual([
        '10.0.0.5',
        'intranet',
        'svc.internal',
        'foo.local',
      ]);
    });

    it('전 항목이 거부되면 유효 허용목록이 비어 모든 발송이 막힌다', () => {
      const { allowed } = sanitizeAllowSuffixes(['localhost', '127.0.0.1']);
      expect(allowed).toEqual([]);
      expect(isAllowedWebhookUrl('https://chat.googleapis.com/x', allowed)).toBe(false);
    });

    it('env 가 비면 기본값(구글챗·슬랙)을 쓴다', () => {
      expect(parseAllowSuffixEnv('').allowed).toEqual([
        'chat.googleapis.com',
        'hooks.slack.com',
      ]);
    });
  });

  describe('주소 축 (조건 6)', () => {
    it('사설·loopback·link-local·멀티캐스트를 거부한다', () => {
      for (const ip of [
        '0.0.0.0',
        '10.0.0.5',
        '100.64.1.1',
        '127.0.0.1',
        '169.254.169.254',
        '172.16.0.1',
        '192.168.1.1',
        '224.0.0.1',
        '::1',
        'fc00::1',
        'fe80::1',
        'ff02::1',
      ])
        expect([ip, isBlockedAddress(ip)]).toEqual([ip, true]);
    });

    it('IPv4-mapped 는 매핑을 푼 뒤 IPv4 규칙을 적용한다', () => {
      expect(isBlockedAddress('::ffff:10.0.0.5')).toBe(true);
      expect(isBlockedAddress('::ffff:142.250.207.106')).toBe(false);
    });

    it('미지정 주소 :: 는 0.0.0.0 과 같이 거부한다 — v6 표기로 우회할 수 없다', () => {
      expect(isBlockedAddress('::')).toBe(true);
      expect(isBlockedAddress('::0')).toBe(true);
      expect(isBlockedAddress('0.0.0.0')).toBe(true);
    });

    it('NAT64·6to4 는 품고 있는 IPv4 를 풀어서 판정한다', () => {
      expect(isBlockedAddress('64:ff9b::a00:1')).toBe(true); //* 10.0.0.1
      expect(isBlockedAddress('64:ff9b::7f00:1')).toBe(true); //* 127.0.0.1
      expect(isBlockedAddress('2002:0a00:0001::1')).toBe(true); //* 6to4 of 10.0.0.1
      expect(isBlockedAddress('64:ff9b::8efa:cf6a')).toBe(false); //* 142.250.207.106
    });

    it('공인 주소는 통과한다', () => {
      expect(isBlockedAddress('142.250.207.106')).toBe(false);
      expect(isBlockedAddress('2404:6800:4004::1')).toBe(false);
    });

    it('허용 접미사 호스트가 사설 대역으로 해석되면 거부한다', async () => {
      const r = await resolveAllowedAddresses('chat.googleapis.com', async () => [
        { address: '10.0.0.5', family: 4 },
      ]);
      expect(r.ok).toBe(false);
      expect(r.reason).toContain('blocked-address');
    });

    it('해석 실패와 결과 0건은 거부한다 (fail-closed)', async () => {
      const failed = await resolveAllowedAddresses('x.example.com', async () => {
        throw new Error('ENOTFOUND');
      });
      expect(failed.ok).toBe(false);
      const empty = await resolveAllowedAddresses('x.example.com', async () => []);
      expect(empty).toMatchObject({ ok: false, reason: 'no-address' });
    });

    it('공인 주소면 통과하고 그 주소를 그대로 돌려준다 (agent lookup 고정용)', async () => {
      const r = await resolveAllowedAddresses('chat.googleapis.com', async () => pub);
      expect(r.ok).toBe(true);
      expect(r.addresses).toEqual(pub);
    });
  });
});
