import { describe, expect, test } from "vitest";
import type { LogEvent } from "@/lib/logger/events";
import {
  enforceForbiddenErrorFieldsPolicy,
  enforceForbiddenFieldsPolicy,
  findForbiddenFields,
  findForbiddenFieldsInError,
  FORBIDDEN_CREDENTIAL_FIELDS,
  FORBIDDEN_LOG_FIELDS,
  FORBIDDEN_PII_FIELDS,
  stripForbiddenFields,
  stripForbiddenFieldsFromError,
} from "@/lib/logger/redact";

type KeysOfUnion<T> = T extends unknown ? keyof T : never;
type EventKeys = KeysOfUnion<LogEvent>;
type ForbiddenHit = Extract<EventKeys, (typeof FORBIDDEN_LOG_FIELDS)[number]>;

/**
 * イベント型のキー集合に §4 の禁止フィールドが1つでも混ざると、
 * この代入が型エラーになりテストファイルがコンパイルできない。
 */
const _noForbiddenOnEventType: ForbiddenHit extends never ? true : never =
  true;
void _noForbiddenOnEventType;

describe("forbidden log fields", () => {
  test("§4 の記録禁止フィールドがすべて FORBIDDEN_LOG_FIELDS に含まれる", () => {
    const required = [
      "facility_name",
      "address",
      "phone",
      "email",
      "manager_name",
      "charger_name",
      "inspector_name",
      "customer_name",
      "notes",
      "comment",
      "memo",
      "note",
      "detail",
    ];
    expect([...FORBIDDEN_PII_FIELDS]).toEqual(required);
    expect([...FORBIDDEN_LOG_FIELDS]).toEqual(
      expect.arrayContaining(required)
    );
  });

  test("認証情報のキーが FORBIDDEN_LOG_FIELDS に含まれる", () => {
    const credentials = [
      "cookie",
      "set-cookie",
      "authorization",
      "token",
      "session",
      "secret",
      "password",
      "api_key",
      // 指示書I 段階4 で追加。cookie 名の正は lib/auth/cookie.ts の SESSION_COOKIE_NAME
      "dwatch_session",
    ];
    expect([...FORBIDDEN_CREDENTIAL_FIELDS]).toEqual(credentials);
    expect([...FORBIDDEN_LOG_FIELDS]).toEqual(
      expect.arrayContaining(credentials)
    );
  });

  test("§4 の13キーと認証9キーの両方を漏れなく持つ", () => {
    // 21 → 22。指示書I 段階4 で dwatch_session を足した分
    expect(FORBIDDEN_LOG_FIELDS).toHaveLength(22);
  });

  describe("認証情報キーの個別ケース", () => {
    const credentialCases: Array<[string, unknown]> = [
      ["cookie", "dwatch_session=abc123"],
      ["set-cookie", "dwatch_session=abc123; HttpOnly"],
      ["authorization", "Bearer abc123"],
      ["token", "abc123"],
      ["session", "abc123"],
      ["secret", "abc123"],
      ["password", "abc123"],
      ["api_key", "abc123"],
      // 指示書I 段階4。cookie 名をキーにして値を渡してしまった場合も止める
      ["dwatch_session", "abc123"],
    ];

    test.each(credentialCases)("%s を検出する", (key, value) => {
      expect(findForbiddenFields({ event: "api.error", [key]: value })).toEqual([
        key,
      ]);
    });

    test.each(credentialCases)("%s を除去する", (key, value) => {
      const stripped = stripForbiddenFields({
        event: "api.error",
        [key]: value,
      });
      expect(stripped).toEqual({ event: "api.error" });
      expect(JSON.stringify(stripped)).not.toContain("abc123");
    });

    test.each(credentialCases)("入れ子の %s も検出する", (key, value) => {
      expect(
        findForbiddenFields({ event: "api.error", headers: { [key]: value } })
      ).toEqual([key]);
    });

    test("HTTPヘッダの大文字表記でも検出する（Authorization / Set-Cookie）", () => {
      expect(
        findForbiddenFields({
          headers: { Authorization: "Bearer x", "Set-Cookie": "a=b" },
        })
      ).toEqual(["Authorization", "Set-Cookie"]);
    });

    test("api_key に似た許可キー（facility_id）は検出しない", () => {
      expect(findForbiddenFields({ facility_id: 1, token_count: 3 })).toEqual(
        []
      );
    });
  });

  describe("findForbiddenFields（検出のみ、副作用なし）", () => {
    test("許可フィールドだけのペイロードは何も検出しない", () => {
      const payload: LogEvent = {
        event: "api.error",
        route: "/api/clean-logs",
        method: "GET",
        error_code: "internal_error",
        error_message: "内部エラーが発生しました",
        http_status: 500,
      };
      expect(findForbiddenFields(payload)).toEqual([]);
    });

    test("禁止フィールド名を検出する", () => {
      expect(
        findForbiddenFields({ event: "api.error", facility_name: "dummy" })
      ).toEqual(["facility_name"]);
    });

    test("入れ子の comment も検出する（内容ではなくキー名）", () => {
      expect(
        findForbiddenFields({ event: "db.write", nested: { comment: "自由記述" } })
      ).toEqual(["comment"]);
    });

    test("配列の中も検出する", () => {
      expect(
        findForbiddenFields({ items: [{ email: "a@example.com" }] })
      ).toEqual(["email"]);
    });

    test("comment_changed はキー名が comment ではないので検出しない", () => {
      expect(findForbiddenFields({ event: "db.write", comment_changed: true })).toEqual(
        []
      );
    });
  });

  describe("stripForbiddenFields（除去。入れ子・配列も対象）", () => {
    test("トップレベルの禁止キーを取り除く", () => {
      expect(
        stripForbiddenFields({ event: "api.error", facility_name: "dummy" })
      ).toEqual({ event: "api.error" });
    });

    test("入れ子・配列の禁止キーも取り除く", () => {
      expect(
        stripForbiddenFields({
          event: "db.write",
          nested: { comment: "秘密", ok: 1 },
          items: [{ email: "a@example.com", facility_id: 1 }],
        })
      ).toEqual({
        event: "db.write",
        nested: { ok: 1 },
        items: [{ facility_id: 1 }],
      });
    });

    test("元のオブジェクトを変更しない", () => {
      const original = { event: "api.error", facility_name: "dummy" };
      stripForbiddenFields(original);
      expect(original).toEqual({ event: "api.error", facility_name: "dummy" });
    });
  });

  describe("enforceForbiddenFieldsPolicy（環境で挙動を分岐）", () => {
    test("非production: 禁止フィールドがあれば例外を投げる", () => {
      expect(() =>
        enforceForbiddenFieldsPolicy(
          { event: "api.error", facility_name: "dummy" },
          false
        )
      ).toThrow(/facility_name/);
    });

    test("非production: 禁止フィールドが無ければそのまま返す", () => {
      const payload = { event: "api.error" as const };
      const result = enforceForbiddenFieldsPolicy(payload, false);
      expect(result).toEqual({ payload, leaked: [] });
    });

    test("production: 例外を投げず、キーを除去した payload と leaked を返す", () => {
      const result = enforceForbiddenFieldsPolicy(
        { event: "api.error", facility_name: "dummy", http_status: 500 },
        true
      );
      expect(result.leaked).toEqual(["facility_name"]);
      expect(result.payload).toEqual({ event: "api.error", http_status: 500 });
    });

    test("production: 除去後の payload に値（facility_nameの中身）が残らない", () => {
      const result = enforceForbiddenFieldsPolicy(
        { event: "api.error", facility_name: "本物の施設名" },
        true
      );
      expect(JSON.stringify(result.payload)).not.toContain("本物の施設名");
    });
  });

  describe("err の独自プロパティ", () => {
    /** Prisma のエラーを模した、meta に値を持つ例外 */
    class FakePrismaError extends Error {
      code = "P2002";
      meta: Record<string, unknown>;
      constructor(message: string, meta: Record<string, unknown>) {
        super(message);
        this.name = "PrismaClientKnownRequestError";
        this.meta = meta;
      }
    }

    test("列挙可能な独自プロパティの禁止キーを検出する", () => {
      const err = new FakePrismaError("boom", { authorization: "Bearer x" });
      expect(findForbiddenFieldsInError(err)).toEqual(["authorization"]);
    });

    test("meta の入れ子も検出する", () => {
      const err = new FakePrismaError("boom", {
        target: { cookie: "dwatch_session=abc123" },
      });
      expect(findForbiddenFieldsInError(err)).toEqual(["cookie"]);
    });

    test("message / stack はキー検査の対象外（非列挙なので拾わない）", () => {
      const err = new Error("boom");
      expect(findForbiddenFieldsInError(err)).toEqual([]);
    });

    test("除去しても message / stack と型名が残る", () => {
      const err = new FakePrismaError("boom", { token: "abc123" });
      const cleaned = stripForbiddenFieldsFromError(err) as Error & {
        code?: string;
        meta?: unknown;
      };
      expect(cleaned).toBeInstanceOf(Error);
      expect(cleaned.constructor.name).toBe("FakePrismaError");
      expect(cleaned.name).toBe("PrismaClientKnownRequestError");
      expect(cleaned.message).toBe("boom");
      expect(cleaned.stack).toBeTruthy();
      // 許可プロパティは残り、禁止キーだけ消える
      expect(cleaned.code).toBe("P2002");
      expect(cleaned.meta).toEqual({});
    });

    test("元の例外オブジェクトを変更しない", () => {
      const err = new FakePrismaError("boom", { token: "abc123" });
      stripForbiddenFieldsFromError(err);
      expect(err.meta).toEqual({ token: "abc123" });
    });

    test("非production: err に禁止キーがあれば例外を投げる", () => {
      const err = new FakePrismaError("boom", { password: "abc123" });
      expect(() => enforceForbiddenErrorFieldsPolicy(err, false)).toThrow(
        /password/
      );
    });

    test("production: 例外を投げず、除去した err と leaked を返す", () => {
      const err = new FakePrismaError("boom", { password: "abc123" });
      const result = enforceForbiddenErrorFieldsPolicy(err, true);
      expect(result.leaked).toEqual(["password"]);
      expect((result.err as { meta?: unknown }).meta).toEqual({});
    });

    test("禁止キーが無ければ同一オブジェクトをそのまま返す", () => {
      const err = new FakePrismaError("boom", { facility_id: 1 });
      const result = enforceForbiddenErrorFieldsPolicy(err, true);
      expect(result.leaked).toEqual([]);
      expect(result.err).toBe(err);
    });

    test("object 以外（文字列 throw）でも落ちない", () => {
      expect(findForbiddenFieldsInError("boom")).toEqual([]);
      expect(stripForbiddenFieldsFromError("boom")).toBe("boom");
      expect(enforceForbiddenErrorFieldsPolicy(undefined, false)).toEqual({
        err: undefined,
        leaked: [],
      });
    });
  });
});
