/**
 * 段階2 の判定（IP制限 → 認証）のテスト。
 * ★Response は組み立てず判定だけを返す設計なので、素の値で全経路を固定できる★
 */
import { describe, expect, it } from "vitest";
import {
  authenticate,
  isApiPath,
  isPublicPath,
  SESSION_COOKIE_NAME,
} from "@/lib/proxy/authenticate";

/** 既定の依存。各テストで必要な分だけ上書きする */
const base = {
  allowedIps: [] as readonly string[],
  allowAllWhenEmpty: true,
  allowDirectAccess: true,
  trustedProxyHops: 1,
  expectedToken: "correct-token",
};

describe("isPublicPath()", () => {
  it("/login と /api/auth/login は認証不要", () => {
    expect(isPublicPath("/login")).toBe(true);
    expect(isPublicPath("/api/auth/login")).toBe(true);
  });

  it("配下も通す", () => {
    expect(isPublicPath("/login/help")).toBe(true);
  });

  it("★/api/auth/logout は認証が要る★（cookie を消すだけだが、公開にする理由がない）", () => {
    expect(isPublicPath("/api/auth/logout")).toBe(false);
  });

  it("★前方一致で誤って通さないこと★", () => {
    expect(isPublicPath("/login-secret")).toBe(false);
    expect(isPublicPath("/dashboard")).toBe(false);
    expect(isPublicPath("/api/dashboard")).toBe(false);
  });
});

describe("isApiPath()", () => {
  it("APIは 401、画面は /login リダイレクト に分けるための判定", () => {
    expect(isApiPath("/api/dashboard")).toBe(true);
    expect(isApiPath("/api")).toBe(true);
    expect(isApiPath("/dashboard")).toBe(false);
    expect(isApiPath("/apiary")).toBe(false);
  });
});

describe("authenticate() — IP制限が先、認証が後", () => {
  it("★IP拒否は認証より先に効く★（正しいトークンを持っていても 403）", () => {
    expect(
      authenticate("/dashboard", "1.2.3.4, 198.51.100.9", "correct-token", {
        ...base,
        allowedIps: ["203.0.113.10"],
        allowAllWhenEmpty: false,
        allowDirectAccess: false,
      })
    ).toEqual({ kind: "deny_ip" });
  });

  it("★IP制限は /login にもかかる★（ログイン画面だけ誰でも開けるのでは意味が薄れる）", () => {
    expect(
      authenticate("/login", "1.2.3.4, 198.51.100.9", null, {
        ...base,
        allowedIps: ["203.0.113.10"],
        allowAllWhenEmpty: false,
        allowDirectAccess: false,
      })
    ).toEqual({ kind: "deny_ip" });
  });

  it("★最重要: 先頭に許可IPを書いた偽装ヘッダは素通りしない★", () => {
    // 攻撃者が X-Forwarded-For: <許可IP> を自分で付け、nginx が実接続元を末尾に追記した形
    expect(
      authenticate("/dashboard", "203.0.113.10, 198.51.100.9", "correct-token", {
        ...base,
        allowedIps: ["203.0.113.10"],
        allowAllWhenEmpty: false,
        allowDirectAccess: false,
      })
    ).toEqual({ kind: "deny_ip" });
  });

  it("末尾が許可IPなら通る（nginx が追記した正常系）", () => {
    expect(
      authenticate("/dashboard", "1.2.3.4, 203.0.113.10", "correct-token", {
        ...base,
        allowedIps: ["203.0.113.10"],
        allowAllWhenEmpty: false,
        allowDirectAccess: false,
      })
    ).toEqual({ kind: "allow" });
  });

  it("★XFF が無く ALLOW_DIRECT_ACCESS=false なら 403★（allowlist が空でも）", () => {
    expect(
      authenticate("/dashboard", null, "correct-token", {
        ...base,
        allowedIps: [],
        allowAllWhenEmpty: true,
        allowDirectAccess: false,
      })
    ).toEqual({ kind: "deny_ip" });
  });

  it("XFF が無くても ALLOW_DIRECT_ACCESS=true なら IP判定は通る（ローカル開発）", () => {
    expect(
      authenticate("/dashboard", null, "correct-token", base)
    ).toEqual({ kind: "allow" });
  });
});

describe("authenticate() — 認証", () => {
  it("cookie が無ければ未認証", () => {
    expect(authenticate("/dashboard", null, null, base)).toEqual({
      kind: "unauthenticated",
    });
  });

  it("cookie の値が違えば未認証", () => {
    expect(authenticate("/dashboard", null, "wrong", base)).toEqual({
      kind: "unauthenticated",
    });
  });

  it("cookie の値が正しければ通る", () => {
    expect(authenticate("/dashboard", null, "correct-token", base)).toEqual({
      kind: "allow",
    });
  });

  it("APIルートも同じ判定（Response の作り分けは proxy.ts の責務）", () => {
    expect(authenticate("/api/dashboard", null, null, base)).toEqual({
      kind: "unauthenticated",
    });
  });

  it("★/login は cookie 無しでも通る★（ここを保護すると誰もログインできない）", () => {
    expect(authenticate("/login", null, null, base)).toEqual({ kind: "allow" });
    expect(authenticate("/api/auth/login", null, null, base)).toEqual({
      kind: "allow",
    });
  });

  it("★AUTH_TOKEN が未設定なら誰も通らない★（設定漏れを全公開にしない）", () => {
    expect(
      authenticate("/dashboard", null, "anything", {
        ...base,
        expectedToken: undefined,
      })
    ).toEqual({ kind: "unauthenticated" });
    expect(
      authenticate("/dashboard", null, "", { ...base, expectedToken: "" })
    ).toEqual({ kind: "unauthenticated" });
  });

  it("★AUTH_TOKEN 未設定でも /login は開ける★（そうでないと設定ミスを直せない）", () => {
    expect(
      authenticate("/login", null, null, { ...base, expectedToken: undefined })
    ).toEqual({ kind: "allow" });
  });
});

describe("cookie 名", () => {
  it("dwatch_session（lib/logger/redact.ts のマスキング対象と揃っていること）", () => {
    expect(SESSION_COOKIE_NAME).toBe("dwatch_session");
  });
});
