/**
 * 環境変数検証の期待値表（指示書I 段階5-B の検査1〜6）。
 *
 * 検査ごとに「引っかかる入力」「引っかからない入力」「本番判定 true/false で
 * 結果が変わること」を固定する。
 * ★エラーメッセージに値が含まれないことのテストを含む（要点2 が後から壊れるのを防ぐ）★
 */
import { describe, expect, it } from "vitest";
import {
  checkEnv,
  databaseWriteCredentials,
  formatFinding,
  hasError,
  type EnvCheckFinding,
} from "@/lib/config/env-check";

/** 全検査を通過する最小構成（本番判定でも findings が空になる値） */
const clean: Record<string, string | undefined> = {
  AUTH_TOKEN: "a-real-secret-value",
  DATABASE_URL: "mysql://user:pw@db.example.internal:3306/clean_watch",
  USE_FIXTURES: "false",
  LOG_LEVEL: "info",
  // ★書き込み用アカウント（指示書AT §3-2）★ — 使うときは部品変数の HOST / NAME も要る
  DATABASE_HOST: "db.example.internal",
  DATABASE_NAME: "clean_watch",
  DATABASE_WRITE_USER: "writer-user-example",
  DATABASE_WRITE_PASSWORD: "writer-password-example",
};

function find(
  findings: EnvCheckFinding[],
  key: string
): EnvCheckFinding | undefined {
  return findings.find((f) => f.key === key);
}

describe("checkEnv() — 正常系", () => {
  it("本番判定でも、正しく埋まっていれば findings は空", () => {
    expect(checkEnv(clean, true)).toEqual([]);
    expect(hasError(checkEnv(clean, true))).toBe(false);
  });

  it("開発機（本番判定 false）でも空", () => {
    expect(checkEnv(clean, false)).toEqual([]);
  });
});

describe("検査1: AUTH_TOKEN", () => {
  it("★changeme のままなら エラー★", () => {
    const f = find(checkEnv({ ...clean, AUTH_TOKEN: "changeme" }, true), "AUTH_TOKEN");
    expect(f?.severity).toBe("error");
  });

  it("★開発機でも エラー（本番判定に関係なく落とす）★", () => {
    const f = find(checkEnv({ ...clean, AUTH_TOKEN: "changeme" }, false), "AUTH_TOKEN");
    expect(f?.severity).toBe("error");
  });

  it("未設定なら エラー", () => {
    expect(
      find(checkEnv({ ...clean, AUTH_TOKEN: undefined }, false), "AUTH_TOKEN")
        ?.severity
    ).toBe("error");
  });

  it("空文字なら エラー", () => {
    expect(
      find(checkEnv({ ...clean, AUTH_TOKEN: "" }, false), "AUTH_TOKEN")?.severity
    ).toBe("error");
  });

  it("ちゃんとした値なら引っかからない", () => {
    expect(find(checkEnv(clean, true), "AUTH_TOKEN")).toBeUndefined();
  });
});

describe("検査2: ALLOW_DIRECT_ACCESS", () => {
  it("★本番判定で true なら エラー★", () => {
    const f = find(
      checkEnv({ ...clean, ALLOW_DIRECT_ACCESS: "true" }, true),
      "ALLOW_DIRECT_ACCESS"
    );
    expect(f?.severity).toBe("error");
  });

  it("★開発機で true なら 警告（落としてはいけない）★", () => {
    const f = find(
      checkEnv({ ...clean, ALLOW_DIRECT_ACCESS: "true" }, false),
      "ALLOW_DIRECT_ACCESS"
    );
    expect(f?.severity).toBe("warn");
    expect(hasError(checkEnv({ ...clean, ALLOW_DIRECT_ACCESS: "true" }, false))).toBe(
      false
    );
  });

  it("未設定なら引っかからない（＝本番の正しい状態）", () => {
    expect(find(checkEnv(clean, true), "ALLOW_DIRECT_ACCESS")).toBeUndefined();
  });

  it("false / 1 / TRUE は（アプリ側で false 扱いなので）引っかからない", () => {
    for (const value of ["false", "1", "TRUE"]) {
      expect(
        find(
          checkEnv({ ...clean, ALLOW_DIRECT_ACCESS: value }, true),
          "ALLOW_DIRECT_ACCESS"
        )
      ).toBeUndefined();
    }
  });
});

describe("検査3: USE_FIXTURES", () => {
  it("★本番判定で false 以外なら 警告★", () => {
    for (const value of [undefined, "true", "yes", ""]) {
      const f = find(
        checkEnv({ ...clean, USE_FIXTURES: value }, true),
        "USE_FIXTURES"
      );
      expect(f?.severity).toBe("warn");
    }
  });

  it("開発機では引っかからない", () => {
    expect(
      find(checkEnv({ ...clean, USE_FIXTURES: "true" }, false), "USE_FIXTURES")
    ).toBeUndefined();
  });

  it("false なら引っかからない", () => {
    expect(find(checkEnv(clean, true), "USE_FIXTURES")).toBeUndefined();
  });
});

describe("検査4/5: DATABASE_URL", () => {
  it("未設定なら エラー", () => {
    expect(
      find(checkEnv({ ...clean, DATABASE_URL: undefined }, false), "DATABASE_URL")
        ?.severity
    ).toBe("error");
  });

  it("★本番判定で localhost なら 警告★", () => {
    const f = find(
      checkEnv(
        { ...clean, DATABASE_URL: "mysql://root@localhost:3306/dwatch_local" },
        true
      ),
      "DATABASE_URL"
    );
    expect(f?.severity).toBe("warn");
  });

  it("★本番判定で 127.0.0.1 なら 警告★", () => {
    const f = find(
      checkEnv(
        { ...clean, DATABASE_URL: "mysql://root@127.0.0.1:3306/dwatch_local" },
        true
      ),
      "DATABASE_URL"
    );
    expect(f?.severity).toBe("warn");
  });

  it("開発機で localhost なら引っかからない（＝ローカル開発の正常な状態）", () => {
    expect(
      find(
        checkEnv(
          { ...clean, DATABASE_URL: "mysql://root@localhost:3306/dwatch_local" },
          false
        ),
        "DATABASE_URL"
      )
    ).toBeUndefined();
  });

  it("リモートホストなら引っかからない", () => {
    expect(find(checkEnv(clean, true), "DATABASE_URL")).toBeUndefined();
  });
});

describe("検査6: LOG_LEVEL", () => {
  it("★debug を弾かないこと（resolveLevel() が開発時に返す値）★", () => {
    expect(
      find(checkEnv({ ...clean, LOG_LEVEL: "debug" }, false), "LOG_LEVEL")
    ).toBeUndefined();
  });

  it("pino のレベル名はすべて通す", () => {
    for (const level of ["trace", "debug", "info", "warn", "error", "fatal"]) {
      expect(
        find(checkEnv({ ...clean, LOG_LEVEL: level }, true), "LOG_LEVEL")
      ).toBeUndefined();
    }
  });

  it("想定外の値なら 警告", () => {
    const f = find(
      checkEnv({ ...clean, LOG_LEVEL: "verbose" }, true),
      "LOG_LEVEL"
    );
    expect(f?.severity).toBe("warn");
  });

  it("未設定なら引っかからない（NODE_ENV から判定されるため）", () => {
    expect(
      find(checkEnv({ ...clean, LOG_LEVEL: undefined }, true), "LOG_LEVEL")
    ).toBeUndefined();
  });
});

describe("★要点2: エラーメッセージに値そのものを出さないこと★", () => {
  const secretPassword = "S3cr3t-P4ssw0rd-DoNotLeak";
  const secretToken = "super-secret-shared-token";

  it("★DATABASE_URL のパスワードが出力に含まれないこと★", () => {
    const env = {
      ...clean,
      DATABASE_URL: `mysql://admin:${secretPassword}@localhost:3306/dwatch_local`,
    };
    const output = checkEnv(env, true).map(formatFinding).join("\n");
    expect(output).not.toContain(secretPassword);
    expect(output).not.toContain("admin");
    // 警告自体は出ていること（何も検出していないから含まれないのでは意味がない）
    expect(output).toContain("DATABASE_URL");
  });

  it("★パースできない DATABASE_URL でも値が漏れないこと★", () => {
    const env = { ...clean, DATABASE_URL: `not-a-url-${secretPassword}` };
    const output = checkEnv(env, true).map(formatFinding).join("\n");
    expect(output).not.toContain(secretPassword);
  });

  it("★AUTH_TOKEN の値が出力に含まれないこと★", () => {
    const env = { ...clean, AUTH_TOKEN: secretToken, LOG_LEVEL: "verbose" };
    const output = checkEnv(env, true).map(formatFinding).join("\n");
    expect(output).not.toContain(secretToken);
  });

  it("★どの検査結果にも、渡した値の文字列が含まれないこと★（総当たり）", () => {
    const env = {
      AUTH_TOKEN: "changeme",
      DATABASE_URL: `mysql://u:${secretPassword}@localhost:3306/d`,
      USE_FIXTURES: "true",
      ALLOW_DIRECT_ACCESS: "true",
      LOG_LEVEL: "verbose",
    };
    const findings = checkEnv(env, true);
    expect(findings.length).toBeGreaterThan(0);
    const output = findings.map(formatFinding).join("\n");
    expect(output).not.toContain(secretPassword);
    // changeme は「キーワード名」として言及してよい唯一の例外（値そのものが公開情報のため）
  });
});

describe("hasError() / formatFinding()", () => {
  it("エラーが1件でもあれば true", () => {
    expect(hasError(checkEnv({ ...clean, AUTH_TOKEN: "changeme" }, false))).toBe(
      true
    );
  });

  it("警告だけなら false（終了コード0）", () => {
    const findings = checkEnv({ ...clean, LOG_LEVEL: "verbose" }, true);
    expect(findings.length).toBeGreaterThan(0);
    expect(hasError(findings)).toBe(false);
  });

  it("★警告とエラーが出力で区別できること★", () => {
    expect(
      formatFinding({ severity: "error", key: "K", message: "m" })
    ).toMatch(/^ERROR: /);
    expect(formatFinding({ severity: "warn", key: "K", message: "m" })).toMatch(
      /^WARN /
    );
  });
});

/**
 * 検査7: 書き込み用アカウント（指示書AT §3-2 / 段階7）
 *   未設定 ＝ warn「書き込みは参照用アカウントで行います」/ 片方だけ ＝ error / 値は出さない
 */
describe("検査7: DATABASE_WRITE_USER / DATABASE_WRITE_PASSWORD", () => {
  const none = { ...clean, DATABASE_WRITE_USER: undefined, DATABASE_WRITE_PASSWORD: undefined };

  it("databaseWriteCredentials(): none / partial / both（空文字は未設定扱い）", () => {
    expect(databaseWriteCredentials({})).toBe("none");
    expect(databaseWriteCredentials({ DATABASE_WRITE_USER: "", DATABASE_WRITE_PASSWORD: "" })).toBe("none");
    expect(databaseWriteCredentials({ DATABASE_WRITE_USER: "u" })).toBe("partial");
    expect(databaseWriteCredentials({ DATABASE_WRITE_PASSWORD: "p" })).toBe("partial");
    expect(databaseWriteCredentials({ DATABASE_WRITE_USER: "u", DATABASE_WRITE_PASSWORD: "" })).toBe("partial");
    expect(databaseWriteCredentials({ DATABASE_WRITE_USER: "u", DATABASE_WRITE_PASSWORD: "p" })).toBe("both");
  });

  it("★2つとも未設定 → 警告（参照用アカウントで書く）★。開発機でも本番でも同じ", () => {
    for (const isProduction of [true, false]) {
      const f = find(checkEnv(none, isProduction), "DATABASE_WRITE_USER");
      expect(f?.severity).toBe("warn");
      expect(f?.message).toContain("参照用アカウント");
    }
    expect(hasError(checkEnv(none, true))).toBe(false);
  });

  it("★片方だけ → エラー★（黙って参照用に倒さない）", () => {
    for (const env of [
      { ...none, DATABASE_WRITE_USER: "u" },
      { ...none, DATABASE_WRITE_PASSWORD: "p" },
    ]) {
      expect(find(checkEnv(env, false), "DATABASE_WRITE_USER")?.severity).toBe("error");
    }
  });

  it("2つとも設定でも DATABASE_HOST / DATABASE_NAME が無ければエラー（接続先を組み立てられない）", () => {
    expect(
      find(checkEnv({ ...clean, DATABASE_HOST: undefined }, false), "DATABASE_WRITE_USER")?.severity
    ).toBe("error");
    expect(
      find(checkEnv({ ...clean, DATABASE_NAME: undefined }, false), "DATABASE_WRITE_USER")?.severity
    ).toBe("error");
  });

  it("2つとも設定し HOST / NAME もあれば何も出ない", () => {
    expect(find(checkEnv(clean, true), "DATABASE_WRITE_USER")).toBeUndefined();
  });

  it("★出力に書き込み用のユーザー名・パスワードが含まれないこと★", () => {
    const secretUser = "write-user-DoNotLeak";
    const secretPassword = "write-pass-DoNotLeak";
    for (const env of [
      { ...none, DATABASE_WRITE_USER: secretUser },
      { ...none, DATABASE_WRITE_PASSWORD: secretPassword },
      { ...clean, DATABASE_WRITE_USER: secretUser, DATABASE_WRITE_PASSWORD: secretPassword, DATABASE_HOST: undefined },
    ]) {
      const output = checkEnv(env, true).map(formatFinding).join("\n");
      // 何かは出ていること（何も検出していないから含まれないのでは意味がない）
      expect(output).toContain("DATABASE_WRITE_USER");
      expect(output).not.toContain(secretUser);
      expect(output).not.toContain(secretPassword);
    }
  });
});
