import { describe, expect, it } from "vitest";
import {
  displayRate,
  errorRate,
  isCompletedTask,
  judgeFacilityStatus,
  judgeGrade,
  launchRate,
  meanCleanRatio,
  operationRate,
  toKpiValue,
} from "@/lib/judge";
import { CLEAN_TASK_STATUS } from "@/lib/config";

/**
 * KPI4指標の期待値表テスト（CLAUDE.md 4.2）
 *
 *   稼働率       = 正常終了（status = 4）の回数 ÷ 清掃予定回数 × 100   ★第11回 P-6=A★
 *   出発成功率   = 開始できた回数（レポート行がある回数）÷ 清掃予定回数 × 100   ★時刻の条件なし（P-6=A）★
 *   清掃完了率   = Σ 1回ごとの完了率 ÷ 件数   ★1回ごとの完了率の単純平均（清掃1回＝1票）★
 *                 （★期間の面積比は廃止。指示書AQ 決定4 / §5-1。D-155 で材料が `clean_ratio` 列から
 *                 `runCompletionRate()` に変わった★）
 *   エラー発生率 = エラー発生回数 ÷ 清掃予定回数 × 100   ★分母は予定回数（P-6=B）。上限で切らない★
 *
 *   ★3つの率はサマリ（`operation_ratio` / `launch_ratio` / `error_ratio`）と同じ式（第11回 P-6。D-158）★。
 *   旧: 稼働率 ＝ 実施回数 ÷ 予定・出発成功率 ＝ 定刻に出発した回数 ÷ 予定・エラー発生率 ＝ エラー ÷ 実施回数
 */

describe("operationRate（稼働率）", () => {
  it("正常終了の回数 ÷ 清掃予定回数（P-6=A）", () => {
    expect(operationRate(141, 145)).toBeCloseTo(97.2413, 3);
    expect(operationRate(100, 100)).toBe(100);
    expect(operationRate(0, 100)).toBe(0);
  });

  it("分母が0なら null（0%ではない）", () => {
    // 「予定が1件もない期間」と「予定はあったが全部失敗」を区別する
    expect(operationRate(0, 0)).toBeNull();
  });
});

describe("launchRate（出発成功率）", () => {
  it("開始できた回数 ÷ 清掃予定回数（時刻の条件なし。P-6=A）", () => {
    // 予定100件・開始できた（レポート行がある）85件 → 85/100
    expect(launchRate(85, 100)).toBe(85);
  });

  it("分母が0なら null", () => {
    expect(launchRate(0, 0)).toBeNull();
  });
});

/**
 * ★清掃完了率の畳み方（meanCleanRatio）の期待値表★（指示書AQ 決定4 / §5-1）
 *
 * | # | 入力（Σ1回ごとの完了率 / 件数） | 期待 | 何を固定するか |
 * |---|---|---|---|
 * | m-1 | 87 / 1 | 87 | 1件はその値のまま |
 * | m-2 | 150 / 2（100% と 50%） | 75 | 単純平均 |
 * | m-3 | 90.4 / 1 | 90.4（表示90% だが判定は ◎） | ★丸めない★（§4.3） |
 * | m-4 | 89.5 / 1 | 89.5（表示90% だが判定は ○） | ★丸めない★（§9.4） |
 * | m-5 | 0 / 1 | 0 | 「0%で清掃した」は 0（null にしない） |
 * | m-6 | 0 / 0 | null | ★件数0は率を出さない（0% にしない）★ |
 * | m-7 | 180 / 2 | 90（★900 にしない★） | ★`× 100` しない（1回ごとの完了率は既に百分率）★ |
 */
describe("meanCleanRatio（清掃完了率＝1回ごとの完了率の単純平均）", () => {
  const mean = (sum: number, count: number) =>
    meanCleanRatio({ cleanRatioSum: sum, cleanRatioCount: count });

  it("m-1: 1件はその値のまま", () => {
    expect(mean(87, 1)).toBe(87);
  });

  it("m-2: 100% と 50% の2件 → 75（清掃1回＝1票）", () => {
    expect(mean(150, 2)).toBe(75);
  });

  it("★m-3: 90.4 は生値のまま（表示すると90%だが、判定は◎）★", () => {
    const r = mean(90.4, 1);
    expect(r).toBe(90.4);
    expect(displayRate(r)).toBe("90%");
    expect(judgeGrade(r)).toBe("◎");
  });

  it("★m-4: 89.5 は生値のまま（表示すると90%だが、判定は○）★", () => {
    const r = mean(89.5, 1);
    expect(r).toBe(89.5);
    expect(displayRate(r)).toBe("90%");
    expect(judgeGrade(r)).toBe("○");
  });

  it("m-5: 0% で実施した1件は 0（null にしない）", () => {
    expect(mean(0, 1)).toBe(0);
  });

  it("★m-6: 件数0は null（0%ではない）★", () => {
    expect(mean(0, 0)).toBeNull();
  });

  it("★m-7: 百分率をもう一度100倍しない（180/2 は 90。900 ではない）★", () => {
    expect(mean(180, 2)).toBe(90);
  });
});

describe("errorRate（エラー発生率）", () => {
  it("★分母は清掃予定回数（P-6=B）★", () => {
    // 予定100件・開始90件・エラー9件 → 9/100 = 9% であって 9/90 = 10% ではない
    expect(errorRate(9, 100)).toBe(9);
  });

  it("予定が0なら null", () => {
    expect(errorRate(0, 0)).toBeNull();
  });

  it("★上限で切らない（D9）★: 予定3・エラー4 → 133.33…", () => {
    expect(errorRate(4, 3)).toBeCloseTo(400 / 3, 10);
  });
});

describe("isCompletedTask（正常終了か。稼働率の分子。P-6=A）", () => {
  it("レポート行が無い（undefined）は false", () => {
    expect(isCompletedTask(undefined)).toBe(false);
  });

  it("status が正常終了なら true", () => {
    expect(isCompletedTask({ status: CLEAN_TASK_STATUS.completed })).toBe(true);
  });

  it.each([0, 1, 2, 3, 5, 6, 255])("status %d は false", (status) => {
    expect(isCompletedTask({ status })).toBe(false);
  });
});

describe("toKpiValue（前期比）", () => {
  it("差分を計算する", () => {
    const v = toKpiValue(91.2, 89.5);
    expect(v.value).toBe(91.2);
    expect(v.prev).toBe(89.5);
    expect(v.diff).toBeCloseTo(1.7, 5);
  });

  it("悪化しているケースは負の差分", () => {
    expect(toKpiValue(85.0, 90.0).diff).toBe(-5);
  });

  it("前期データなし（新規施設）は prev / diff とも null", () => {
    const v = toKpiValue(86.0, null);
    expect(v.prev).toBeNull();
    expect(v.diff).toBeNull();
  });
});

describe("judgeFacilityStatus（良好/注意/要改善/判定なし）", () => {
  it("稼働率ベースの暫定判定", () => {
    expect(judgeFacilityStatus(96.3)).toBe("good");
    expect(judgeFacilityStatus(90.0)).toBe("good"); // 境界（以上）
    expect(judgeFacilityStatus(89.9)).toBe("warning");
    expect(judgeFacilityStatus(75.0)).toBe("warning"); // 境界（以上）
    expect(judgeFacilityStatus(74.9)).toBe("bad");
  });

  it("0%は「判定なし」ではない（実施予定があって1回も実施できていない＝要改善）", () => {
    expect(judgeFacilityStatus(0)).toBe("bad");
  });

  // 決定1（2026-09-11 ユーザー確定。docs/decisions.md D-109）: 分母0（＝その期間に
  // 清掃予定が1件もない。週の初め・月の初めに常時発生する。D-106）は「要改善」ではなく
  // 「判定なし」に変えた。以前は null を "bad" に寄せていたため、週の初めに全施設が
  // 赤い「要改善」バッジになる不具合があった。
  it("null（清掃予定が1件もない期間）は判定なしにする（決定1）", () => {
    expect(judgeFacilityStatus(null)).toBe("no_data");
  });
});
