/**
 * 指示書I 段階1 の期待値表をそのまま固定したテスト。
 * ★表を先に確定させてから書いている（CLAUDE.md §6-2）。行を削らないこと★
 */
import { describe, expect, it } from "vitest";
import { clientIp, isAllowedIp } from "@/lib/auth/client-ip";
import {
  ALLOW_ALL_WHEN_EMPTY,
  ALLOW_DIRECT_ACCESS,
  TRUSTED_PROXY_HOPS,
} from "@/lib/config/allowed-ips";

describe("clientIp() — X-Forwarded-For から接続元を取る", () => {
  const cases: Array<{
    name: string;
    header: string | null;
    hops: number;
    expected: string | null;
  }> = [
    {
      name: "正常系のメインケース: nginx が末尾に追記する（D-077 項5）。先頭の自称は信じない",
      header: "1.2.3.4, 203.0.113.10",
      hops: 1,
      expected: "203.0.113.10",
    },
    {
      name: "単一要素（クライアントが XFF を送らず nginx が1個足した正常系でもあり得る）",
      header: "203.0.113.10",
      hops: 1,
      expected: "203.0.113.10",
    },
    {
      name: "多段でも末尾",
      header: "1.2.3.4, 5.6.7.8, 203.0.113.10",
      hops: 1,
      expected: "203.0.113.10",
    },
    {
      name: "★将来2段構成になった場合★: 末尾から2番目が本当のクライアント（10.0.0.5 は LB の内部IP）",
      header: "1.2.3.4, 203.0.113.10, 10.0.0.5",
      hops: 2,
      expected: "203.0.113.10",
    },
    {
      name: "★要素数が hops に満たない → null。先頭で埋めないこと★",
      header: "1.2.3.4, 203.0.113.10",
      hops: 3,
      expected: null,
    },
    {
      name: "前後の空白を落とす",
      header: "  203.0.113.10  ",
      hops: 1,
      expected: "203.0.113.10",
    },
    {
      name: "末尾が空 → 取れない扱い",
      header: "1.2.3.4,",
      hops: 1,
      expected: null,
    },
    { name: "空文字", header: "", hops: 1, expected: null },
    {
      name: "ヘッダ無し ＝ nginx を通っていない（直接アクセス）",
      header: null,
      hops: 1,
      expected: null,
    },
    {
      name: "★同名ヘッダが複数行来ても Headers.get() が ', ' で連結するので分割で足りる★",
      header: "1.2.3.4, 5.6.7.8, 203.0.113.10",
      hops: 1,
      expected: "203.0.113.10",
    },
    {
      name: "★IPv6。正規化しない★",
      header: "1.2.3.4, ::1",
      hops: 1,
      expected: "::1",
    },
    {
      name: "★IPv4射影アドレス。203.0.113.10 に展開しない★",
      header: "1.2.3.4, ::ffff:203.0.113.10",
      hops: 1,
      expected: "::ffff:203.0.113.10",
    },
    {
      name: "★ポート付き。ポートを剥がさない★",
      header: "1.2.3.4, [2001:db8::1]:54321",
      hops: 1,
      expected: "[2001:db8::1]:54321",
    },
  ];

  for (const { name, header, hops, expected } of cases) {
    it(`${name}（header=${JSON.stringify(header)} / hops=${hops}）`, () => {
      expect(clientIp(header, hops)).toBe(expected);
    });
  }

  it("undefined（ヘッダ未取得）も null 扱い", () => {
    expect(clientIp(undefined, 1)).toBeNull();
  });

  it("★先頭の自称IPを絶対に返さないこと★（IP制限の素通りを防ぐ最重要の性質）", () => {
    // 攻撃者が許可IPを先頭に詐称したケース
    expect(clientIp("203.0.113.10, 198.51.100.9", 1)).toBe("198.51.100.9");
    expect(clientIp("203.0.113.10, 198.51.100.9", 1)).not.toBe("203.0.113.10");
  });
});

describe("isAllowedIp() — 3軸（ip が null か / allowlist が空か / 直接アクセスを許すか）", () => {
  const cases: Array<{
    row: number;
    ip: string | null;
    allowedIps: readonly string[];
    allowAllWhenEmpty: boolean;
    allowDirectAccess: boolean;
    expected: boolean;
    note: string;
  }> = [
    {
      row: 1,
      ip: "203.0.113.10",
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: false,
      allowDirectAccess: false,
      expected: true,
      note: "一致",
    },
    {
      row: 2,
      ip: "203.0.113.11",
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: false,
      allowDirectAccess: false,
      expected: false,
      note: "不一致",
    },
    {
      row: 3,
      ip: "203.0.113.10",
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: true,
      allowDirectAccess: false,
      expected: true,
      note: "一致（allowAllWhenEmpty は空でないので無関係）",
    },
    {
      row: 4,
      ip: "203.0.113.11",
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: true,
      allowDirectAccess: false,
      expected: false,
      note: "★allowlist が空でなければ allowAllWhenEmpty は効かない★",
    },
    {
      row: 5,
      ip: "203.0.113.10",
      allowedIps: [],
      allowAllWhenEmpty: true,
      allowDirectAccess: false,
      expected: true,
      note: "制限なし（開発時の既定）",
    },
    {
      row: 6,
      ip: "203.0.113.10",
      allowedIps: [],
      allowAllWhenEmpty: false,
      allowDirectAccess: false,
      expected: false,
      note: "★全拒否（本番想定の設定ミス検知）★",
    },
    {
      row: 7,
      ip: null,
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: false,
      allowDirectAccess: false,
      expected: false,
      note: "直接アクセスは拒否",
    },
    {
      row: 8,
      ip: null,
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: true,
      allowDirectAccess: false,
      expected: false,
      note: "同上",
    },
    {
      row: 9,
      ip: null,
      allowedIps: [],
      allowAllWhenEmpty: true,
      allowDirectAccess: false,
      expected: false,
      note: "★既定状態（allowlist 空）でも直接アクセスは通さない★",
    },
    {
      row: 10,
      ip: null,
      allowedIps: [],
      allowAllWhenEmpty: false,
      allowDirectAccess: false,
      expected: false,
      note: "同上",
    },
    {
      row: 11,
      ip: null,
      allowedIps: [],
      allowAllWhenEmpty: true,
      allowDirectAccess: true,
      expected: true,
      note: "★ローカル開発。ALLOW_DIRECT_ACCESS=true を明示したときだけ通る★",
    },
    {
      row: 12,
      ip: null,
      allowedIps: ["203.0.113.10"],
      allowAllWhenEmpty: false,
      allowDirectAccess: true,
      expected: true,
      note: "★直接アクセスを許した以上、照合すべきIPが無いので通す★",
    },
  ];

  for (const c of cases) {
    it(`行${c.row}: ip=${JSON.stringify(c.ip)} / allowedIps=${JSON.stringify(
      c.allowedIps
    )} / allowAllWhenEmpty=${c.allowAllWhenEmpty} / allowDirectAccess=${
      c.allowDirectAccess
    } → ${c.expected}（${c.note}）`, () => {
      expect(
        isAllowedIp(
          c.ip,
          c.allowedIps,
          c.allowAllWhenEmpty,
          c.allowDirectAccess
        )
      ).toBe(c.expected);
    });
  }

  it("★ip が null のとき allowAllWhenEmpty は結果を一切変えない★（行7〜10 の性質を明示）", () => {
    for (const allowAllWhenEmpty of [true, false]) {
      for (const allowedIps of [[], ["203.0.113.10"]]) {
        expect(isAllowedIp(null, allowedIps, allowAllWhenEmpty, false)).toBe(
          false
        );
        expect(isAllowedIp(null, allowedIps, allowAllWhenEmpty, true)).toBe(
          true
        );
      }
    }
  });

  it("★CIDR は解釈しない（完全一致のみ）★ — P1 の割り切り", () => {
    expect(isAllowedIp("192.0.2.5", ["192.0.2.0/24"], false, false)).toBe(
      false
    );
    // 書いた文字列そのものとしてなら一致する（＝ CIDR として動いてはいない証拠）
    expect(isAllowedIp("192.0.2.0/24", ["192.0.2.0/24"], false, false)).toBe(
      true
    );
  });

  it("★表記ゆれは正規化しないので拒否になる★", () => {
    expect(
      isAllowedIp("::ffff:203.0.113.10", ["203.0.113.10"], false, false)
    ).toBe(false);
    expect(
      isAllowedIp("[2001:db8::1]:54321", ["2001:db8::1"], false, false)
    ).toBe(false);
  });
});

describe("lib/config/allowed-ips.ts の既定値", () => {
  it("TRUSTED_PROXY_HOPS は 1（LB と nginx は同一＝1段。D-079 項1）", () => {
    expect(TRUSTED_PROXY_HOPS).toBe(1);
  });

  it("ALLOW_ALL_WHEN_EMPTY の既定は true（開発を止めないため。★本番では false にする★）", () => {
    expect(ALLOW_ALL_WHEN_EMPTY).toBe(true);
  });

  it("★ALLOW_DIRECT_ACCESS は環境変数由来で、テスト実行時（未設定）は false になること★", () => {
    // `=== "true"` なので未設定は false。`!== "false"` にすると ここが true になって落ちる
    expect(ALLOW_DIRECT_ACCESS).toBe(process.env.ALLOW_DIRECT_ACCESS === "true");
    if (process.env.ALLOW_DIRECT_ACCESS === undefined) {
      expect(ALLOW_DIRECT_ACCESS).toBe(false);
    }
  });
});
