import { describe, expect, it } from "vitest";
import { COMPLETION_DROP, RESCUE_THRESHOLDS } from "@/lib/config";
import {
  countNotExecutedStreak,
  facilityDayRate,
  foldRescueDays,
  isEscalationL3,
  judgeCompletionDrop,
  judgeRescue,
  type RescueRowInput,
} from "../rescue";

/**
 * 要レスキュー店舗の判定（指示書AI 段階3 / ユーザー指示 2026-09-15）
 *
 * ★★このファイルは実装より先に書かれた★★（`CLAUDE.md` §6-2 / §9.4
 * 「期待値表を先に作り、それをテストで固定する」）。表の番号は指示書AI 表1〜表5 に対応する。
 *
 * 3条件:
 *   R-1 未実施が3日以上連続（＝出発失敗と同義。★`departed === false` の連続★。【未決1】案A。D-156）
 *   R-2 連続問題レベル3（★既存の `judgeEscalationLevel()` をそのまま使う★。数え直さない）
 *   R-3 完了率が前回比20%以上の急落（★差ではなく比★。ユーザー確定「比で」）
 */

/**
 * 1行（＝1清掃エリアの1回の清掃）を作る。★★は未実施＝`departed: false` / `completion_rate: null`★。
 * ★`departed` の既定は「`completion_rate` が null でなければ出発した」★ — `departed` と `completion_rate` が
 *   食い違う行（防御。P-5=B 以降 DB からは来ない）は表5 で `departed` を明示して作る（【未決1】案A。D-156 / 指示書AU D3）。
 */
function row(
  date: string,
  completionRate: number | null,
  time = "02:00:00",
  departed = completionRate !== null
): RescueRowInput {
  return {
    scheduled_at: `${date}T${time}+09:00`,
    completion_rate: completionRate,
    departed,
  };
}

/** 未実施の1行（`clean_task_reports` の行が無い清掃。指示書AQ 1-1） */
function notExecuted(date: string, time = "02:00:00"): RescueRowInput {
  return row(date, null, time);
}

/** 正常に終わった1行（★`rate` は1回ごとの完了率＝`runCompletionRate()` の戻り。D-155★） */
function done(date: string, rate: number, time = "02:00:00"): RescueRowInput {
  return row(date, rate, time);
}

/**
 * ★面積つきの1行★（表3 / 表6 用）。
 *
 * ★実際の呼び出し側は `ConsecutiveInput`（`lib/db/clean-log-mapping.ts`）をそのまま渡すので、
 *   `clean_area` / `task_area` が**付いたまま**届く★。★判定がそれを読まないこと★を固定するために使う。
 *   （★オブジェクトリテラルを直接 `RescueRowInput` に代入すると余分なキーで型エラーになる★ので、
 *   一度変数に受けてから返す）
 */
function doneWithArea(
  date: string,
  rate: number,
  cleanArea: number,
  taskArea: number,
  time = "02:00:00"
): RescueRowInput {
  const withArea = {
    scheduled_at: `${date}T${time}+09:00`,
    completion_rate: rate,
    departed: true,
    clean_area: cleanArea,
    task_area: taskArea,
  };
  return withArea;
}

// ---------------------------------------------------------------- 表1: R-1

describe("表1: R-1（未実施が3日以上連続）", () => {
  /** 指示書AI 表1 は「当該日時点の連続数」で判定する。★当該日＝最後の要素★ */
  const streak = (rows: readonly RescueRowInput[]) =>
    countNotExecutedStreak(foldRescueDays(rows));
  const hit = (rows: readonly RescueRowInput[]) =>
    streak(rows) >= RESCUE_THRESHOLDS.notExecutedDays;

  it("1-1: [○] は false（1日・正常）", () => {
    expect(hit([done("2026-09-01", 95)])).toBe(false);
    expect(streak([done("2026-09-01", 95)])).toBe(0);
  });

  it("1-2: [★] は false（未実施1日）", () => {
    expect(hit([notExecuted("2026-09-01")])).toBe(false);
    expect(streak([notExecuted("2026-09-01")])).toBe(1);
  });

  it("1-3: [★, ★] は false（2日）", () => {
    const rows = [notExecuted("2026-09-01"), notExecuted("2026-09-02")];
    expect(streak(rows)).toBe(2);
    expect(hit(rows)).toBe(false);
  });

  it("★1-4: [★, ★, ★] は true（3日。★境界★）", () => {
    const rows = [
      notExecuted("2026-09-01"),
      notExecuted("2026-09-02"),
      notExecuted("2026-09-03"),
    ];
    expect(streak(rows)).toBe(3);
    expect(hit(rows)).toBe(true);
  });

  it("1-5: [★, ★, ★, ★] は true（4日以上も拾う。「3日以上」）", () => {
    const rows = [
      notExecuted("2026-09-01"),
      notExecuted("2026-09-02"),
      notExecuted("2026-09-03"),
      notExecuted("2026-09-04"),
    ];
    expect(streak(rows)).toBe(4);
    expect(hit(rows)).toBe(true);
  });

  it("1-6: [★, ○, ★, ★] は false（途中で切れて2日）", () => {
    const rows = [
      notExecuted("2026-09-01"),
      done("2026-09-02", 95),
      notExecuted("2026-09-03"),
      notExecuted("2026-09-04"),
    ];
    expect(streak(rows)).toBe(2);
    expect(hit(rows)).toBe(false);
  });

  it("★1-7: [★, ★, ★, ○] は false（当該日が正常なら、過去に3日連続があっても拾わない）★", () => {
    const rows = [
      notExecuted("2026-09-01"),
      notExecuted("2026-09-02"),
      notExecuted("2026-09-03"),
      done("2026-09-04", 95),
    ];
    expect(streak(rows)).toBe(0);
    expect(hit(rows)).toBe(false);
  });

  it("1-8: [] は false（行なし）", () => {
    expect(streak([])).toBe(0);
    expect(hit([])).toBe(false);
  });

  it("★1-9: 予定のない日を挟んでも数える（カレンダー日ではなくスケジュール存在日。D-026）★", () => {
    // 9/8 は予定なし。カレンダー日で数えると 9/9〜9/10 の2日に切れて取りこぼす
    const rows = [
      notExecuted("2026-09-07"),
      notExecuted("2026-09-09"),
      notExecuted("2026-09-10"),
    ];
    expect(streak(rows)).toBe(3);
    expect(hit(rows)).toBe(true);
  });

  it("★1-10: 同一日に2台、片方だけ未実施ならその日は「未実施の日」（OR。D-109 A案）★", () => {
    const rows = [notExecuted("2026-09-01"), done("2026-09-01", 90, "03:00:00")];
    // ★未実施の行は完了率の件数に入れない★（完了率は実施した台だけで出す）
    expect(foldRescueDays(rows)).toEqual([
      { date: "2026-09-01", not_executed: true, clean_ratio_sum: 90, clean_ratio_count: 1 },
    ]);
    // ★3日ぶん重ねれば拾う★
    const threeDays = [
      ...rows,
      notExecuted("2026-09-02"),
      done("2026-09-02", 90, "03:00:00"),
      notExecuted("2026-09-03"),
      done("2026-09-03", 90, "03:00:00"),
    ];
    expect(streak(threeDays)).toBe(3);
    expect(hit(threeDays)).toBe(true);
  });

  it("1-11: 同一日に同一ロボットが2回清掃、片方だけ未実施でも1日に畳む（決定2）", () => {
    // ★この入力は 1-10 と構造が同じ★ — 「1日に何行あるか」だけが問題で、
    //   どの台の行かは畳み込みに影響しない（D-026 は日単位であって回数単位ではない）
    const rows = [
      done("2026-09-01", 90, "02:00:00"),
      notExecuted("2026-09-01", "22:00:00"),
    ];
    expect(foldRescueDays(rows)).toHaveLength(1);
    expect(foldRescueDays(rows)[0].not_executed).toBe(true);
    expect(streak(rows)).toBe(1);
  });
});

// ---------------------------------------------------------------- 表2: R-2

describe("表2: R-2（連続問題レベル3）", () => {
  // ★★既存の `judgeEscalationLevel()` をそのまま使う。新しく数え直さない★★
  //   （D-109 の再発防止「連続日数の算出は1本に集約する」。指示書AI §13-6）
  it("2-1: 2日は false（L2）", () => {
    expect(isEscalationL3(2)).toBe(false);
  });

  it("★2-2: 3日は true（L3。★境界★）", () => {
    expect(isEscalationL3(3)).toBe(true);
  });

  it("2-3: 10日は true", () => {
    expect(isEscalationL3(10)).toBe(true);
  });

  it("2-4: 0日は false", () => {
    expect(isEscalationL3(0)).toBe(false);
  });

  it("1日は false（L1）— 境界の下側", () => {
    expect(isEscalationL3(1)).toBe(false);
  });
});

// ---------------------------------------------------------------- 表5（指示書AS）: 未実施の数え方

/**
 * ★★指示書AS §4 表5 — 未実施の数え方（【未決1】案A。D-156）★★
 *
 * | #  | 清掃                        | departed | completion_rate | R-1 の未実施日 |
 * |----|-----------------------------|----------|-----------------|----------------|
 * | U1 | レポートの行が無い           | false    | null            | 数える          |
 * | U2 | ★`departed` と `completion_rate` が食い違う行（防御）★ | ★true★  | ★null★ | ★数えない★ |
 * | U3 | 行がある・50/100             | true     | 50.0            | 数えない        |
 * | U4 | 行がある・100/100            | true     | 100.0           | 数えない        |
 * | U5 | 同じ施設・同じ日に U1 と U4  | —        | —               | その日を1日数える（OR） |
 */
describe("表5（指示書AS）: R-1 の未実施は departed で数える", () => {
  const D = "2026-09-01";
  const notExecutedOf = (rows: readonly RescueRowInput[]) =>
    foldRescueDays(rows).map((d) => d.not_executed);

  it("U1 レポートの行が無い → 未実施の日", () => {
    expect(notExecutedOf([row(D, null, "02:00:00", false)])).toEqual([true]);
  });

  it("★U2 `departed` と `completion_rate` が食い違う行（防御。departed true・completion_rate null）→ 未実施の日に数えない★", () => {
    const days = foldRescueDays([row(D, null, "02:00:00", true)]);
    expect(days.map((d) => d.not_executed)).toEqual([false]);
    // ★完了率の Σ と件数には入らない（null は数えない）★
    expect(days[0].clean_ratio_count).toBe(0);
    expect(facilityDayRate(days)).toBeNull();
  });

  it("U3 行がある・50/100 → 数えない", () => {
    expect(notExecutedOf([row(D, 50, "02:00:00", true)])).toEqual([false]);
  });

  it("U4 行がある・100/100 → 数えない", () => {
    expect(notExecutedOf([row(D, 100, "02:00:00", true)])).toEqual([false]);
  });

  it("U5 同じ日に U1 と U4 → その日を1日数える（OR）", () => {
    const days = foldRescueDays([
      row(D, null, "02:00:00", false),
      row(D, 100, "03:00:00", true),
    ]);
    expect(days.map((d) => d.not_executed)).toEqual([true]);
    expect(countNotExecutedStreak(days)).toBe(1);
  });

  it("★U2 が3日続いても R-1 にならない（出発しているので現場に誤った連絡を飛ばさない）★", () => {
    const rows = ["2026-09-01", "2026-09-02", "2026-09-03"].map((d) =>
      row(d, null, "02:00:00", true)
    );
    expect(countNotExecutedStreak(foldRescueDays(rows))).toBe(0);
  });
});

// ---------------------------------------------------------------- 表3: 施設日完了率

/**
 * ★表3: 施設日完了率（facilityDayRate）＝ その日の1回ごとの完了率の単純平均★
 *   （指示書AQ 決定4 / §5-1・D-155。★2026-09-30 に面積加重から変更★）
 *
 * ★★判定は面積を読まず、渡された `completion_rate` だけを使う★★ — 1回ごとの完了率を面積から作るのは
 *   `runCompletionRate()`（データ層の `completionRateFromReport()` 経由）の仕事で、ここではない。
 *   3-8・3-9 は「面積が付いたまま届いても判定がそれを読まない」ことの確認として残す。
 *
 * | # | その日の行（1回ごとの完了率 / 面積） | 期待 | 何を固定するか |
 * |---|---|---|---|
 * | 3-1 | [90] | 90 | 1行はその値 |
 * | 3-2 | [90, 10] | 50 | 単純平均 |
 * | ★3-3 | [90（900/1000㎡）, 10（10/100㎡）] | ★50★（面積加重なら 82.7…） | ★面積で重み付けしない★ |
 * | 3-4 | [0] | 0 | 0%で実施した（`null` ではない） |
 * | ★3-5 | [未実施] | null | ★0除算を捏造しない★ |
 * | 3-6 | [] | null | 行なし |
 * | 3-7 | [未実施, 90] | 90 | ★未実施行は分母に入れない★（0% として混ぜない） |
 * | ★3-8 | [61.0（1628/1000㎡ ＝ 面積比162.8%）] | ★61★ | ★判定は面積を読まない★（渡された `completion_rate` だけ） |
 * | ★3-9 | [100（138/1000㎡ ＝ 面積比13.8%）] | ★100★ | 同上（逆向き） |
 * | 3-10 | [89.5] | 89.5 | ★丸めない★（§4.3） |
 */
describe("表3: 施設日完了率（facilityDayRate）＝ 1回ごとの完了率の単純平均", () => {
  /** ★同じ日の行を畳んでから率を出す★（`judgeRescue()` と同じ経路） */
  const dayRate = (rows: readonly RescueRowInput[]) => facilityDayRate(foldRescueDays(rows));
  const D = "2026-09-01";

  it("3-1: [90] → 90", () => {
    expect(dayRate([done(D, 90)])).toBe(90);
  });

  it("3-2: [90, 10] → 50", () => {
    expect(dayRate([done(D, 90), done(D, 10, "03:00:00")])).toBe(50);
  });

  it("★3-3: [90（900/1000㎡）, 10（10/100㎡）] → 50（★面積加重なら 82.7…★）★", () => {
    // ★★この1本が「面積で重み付けしないこと」を固定する★★ — 3-1/3-2 は面積加重でも通る
    const actual = dayRate([
      doneWithArea(D, 90, 900, 1000),
      doneWithArea(D, 10, 10, 100, "03:00:00"),
    ]);
    expect(actual).toBe(50);
    expect(actual).not.toBeCloseTo((910 / 1100) * 100, 5);
  });

  it("3-4: [0] → 0（★0%で実施した。`null` ではない★）", () => {
    expect(dayRate([done(D, 0)])).toBe(0);
  });

  it("★3-5: [未実施] → null（0除算を捏造しない）★", () => {
    expect(dayRate([notExecuted(D)])).toBeNull();
  });

  it("3-6: [] → null", () => {
    expect(facilityDayRate([])).toBeNull();
    expect(dayRate([])).toBeNull();
  });

  it("★3-7: [未実施, 90] → 90（未実施行は分母に入れない。0% として混ぜない）★", () => {
    expect(dayRate([notExecuted(D), done(D, 90, "03:00:00")])).toBe(90);
  });

  it("★3-8: 面積比162.8%の面積が付いていても、渡された completion_rate の 61 を使う（判定は面積を読まない）★", () => {
    expect(dayRate([doneWithArea(D, 61, 1628, 1000)])).toBe(61);
  });

  it("★3-9: 面積比13.8%の面積が付いていても、渡された completion_rate の 100 を使う（判定は面積を読まない）★", () => {
    expect(dayRate([doneWithArea(D, 100, 138, 1000)])).toBe(100);
  });

  it("3-10: [89.5] → 89.5（丸めない）", () => {
    expect(dayRate([done(D, 89.5)])).toBe(89.5);
  });
});

// ---------------------------------------------------------------- 表4: R-3

describe("表4: R-3（急落。dropRatio = 0.2）", () => {
  // ★閾値は設定値から読む（ハードコード禁止。`CLAUDE.md` §7）★
  it("前提: 既定の閾値は 0.2（比。★20 ではない★）", () => {
    expect(COMPLETION_DROP.dropRatio).toBe(0.2);
  });

  const dropped = (prev: number | null, cur: number | null) =>
    judgeCompletionDrop(prev, cur).dropped;

  it("★4-1: 100 → 80（ちょうど20%低下）は true（境界は「以上」）★", () => {
    expect(dropped(100, 80)).toBe(true);
  });

  it("4-2: 100 → 80.1（19.9%低下）は false（★境界の反対側★）", () => {
    expect(dropped(100, 80.1)).toBe(false);
  });

  it("4-3: 100 → 79.9（20.1%低下）は true", () => {
    expect(dropped(100, 79.9)).toBe(true);
  });

  it("★4-4: 30 → 18（40%低下）は true（差なら12ポイントで拾えない。比を選んだ意味）★", () => {
    expect(dropped(30, 18)).toBe(true);
    expect(judgeCompletionDrop(30, 18).drop_ratio).toBeCloseTo(0.4, 10);
  });

  it("4-5: 80 → 60（25%低下）は true", () => {
    expect(dropped(80, 60)).toBe(true);
  });

  it("★4-6: 60 → 80（上昇）は false（決定E。絶対値で見ない）★", () => {
    expect(dropped(60, 80)).toBe(false);
  });

  it("★4-7: 0 → 0 は false（決定B。0で割らない）★", () => {
    expect(dropped(0, 0)).toBe(false);
    expect(judgeCompletionDrop(0, 0).drop_ratio).toBeNull();
  });

  it("★4-8: 0 → 50 は false（決定B）★", () => {
    expect(dropped(0, 50)).toBe(false);
    expect(judgeCompletionDrop(0, 50).drop_ratio).toBeNull();
  });

  it("★4-9: null → 50（前回が未実施）は false（決定C。比べる相手が無い）★", () => {
    expect(dropped(null, 50)).toBe(false);
    expect(judgeCompletionDrop(null, 50).drop_ratio).toBeNull();
  });

  it("★4-10: 50 → null（当日が未実施）は false（決定D。R-1 で拾う）★", () => {
    expect(dropped(50, null)).toBe(false);
    expect(judgeCompletionDrop(50, null).drop_ratio).toBeNull();
  });

  it("4-11: null → null は false", () => {
    expect(dropped(null, null)).toBe(false);
  });

  it("★4-12: 前回が存在しない（初日）は false（比べる相手が無い）★", () => {
    // 初日は `judgeRescue()` から見ると「前回の日が無い」＝ previous が null
    const judgement = judgeRescue({
      rows: [done("2026-09-01", 10)],
      consecutive_problem_days: 0,
    });
    expect(judgement.previous_completion_rate).toBeNull();
    expect(judgement.drop_ratio).toBeNull();
    expect(judgement.reasons).not.toContain("completion_drop");
  });

  it("★★4-13: 予定のない日を挟んでも「前回のスケジュール存在日」と比べる（決定A）★★", () => {
    // 施設7 の実データ: 09-07 が 94.7% → 09-09 が 72.5%（09-08 は予定なし）。
    // ★カレンダー前日（09-08）と比べる実装だと算出不可になり、拾えない★
    const judgement = judgeRescue({
      rows: [done("2026-09-07", 94.7), done("2026-09-09", 72.5)],
      consecutive_problem_days: 0,
    });
    expect(judgement.previous_completion_rate).toBeCloseTo(94.7, 10);
    expect(judgement.completion_rate).toBeCloseTo(72.5, 10);
    expect(judgement.drop_ratio).toBeCloseTo((94.7 - 72.5) / 94.7, 10);
    expect(judgement.reasons).toContain("completion_drop");
    expect(judgement.required).toBe(true);
  });
});

// ---------------------------------------------------------------- 表5: 合成

describe("表5: 3条件の合成（judgeRescue）", () => {
  /** R-1 だけを立てる行（3日連続で未実施） */
  const notExecuted3Days = [
    notExecuted("2026-09-01"),
    notExecuted("2026-09-02"),
    notExecuted("2026-09-03"),
  ];
  /** どの条件にも当たらない行 */
  const healthy = [done("2026-09-01", 95), done("2026-09-02", 95)];
  /** R-3 だけを立てる行（100% → 79% ＝ 21%低下） */
  const droppedRows = [done("2026-09-01", 100), done("2026-09-02", 79)];

  it("5-1: 3つとも false なら要レスキューでない", () => {
    const j = judgeRescue({ rows: healthy, consecutive_problem_days: 0 });
    expect(j.required).toBe(false);
    expect(j.reasons).toEqual([]);
  });

  it("5-2: R-1 だけ → 要レスキュー（理由: 未実施3日以上）", () => {
    // ★実データでは未実施日は問題日でもあるので R-2 も同時に立つ★。
    //   ここは合成そのものを確かめるため、連続問題日数を 0 として切り離している
    const j = judgeRescue({ rows: notExecuted3Days, consecutive_problem_days: 0 });
    expect(j.required).toBe(true);
    expect(j.reasons).toEqual(["not_executed_streak"]);
    expect(j.not_executed_days).toBe(3);
  });

  it("5-3: R-2 だけ → 要レスキュー（理由: L3）", () => {
    const j = judgeRescue({ rows: healthy, consecutive_problem_days: 3 });
    expect(j.required).toBe(true);
    expect(j.reasons).toEqual(["escalation_l3"]);
    expect(j.consecutive_problem_days).toBe(3);
  });

  it("5-4: R-3 だけ → 要レスキュー（理由: 急落）", () => {
    const j = judgeRescue({ rows: droppedRows, consecutive_problem_days: 0 });
    expect(j.required).toBe(true);
    expect(j.reasons).toEqual(["completion_drop"]);
    expect(j.drop_ratio).toBeCloseTo(0.21, 10);
  });

  it("★5-5: 3つとも true → 理由を3つとも返す（1つに絞らない）★", () => {
    // ★R-1 と R-3 が同時に立つのは「同じ日に未実施の台と実施した台が混在する」とき★
    //   （1-10 の OR と 3-7 の「未実施行は分母に入れない」が同時に効く）。実データにありうる形
    const rows = [
      notExecuted("2026-09-01"),
      done("2026-09-01", 90, "03:00:00"),
      notExecuted("2026-09-02"),
      done("2026-09-02", 90, "03:00:00"),
      notExecuted("2026-09-03"),
      done("2026-09-03", 50, "03:00:00"),
    ];
    const j = judgeRescue({ rows, consecutive_problem_days: 4 });
    expect(j.required).toBe(true);
    expect(j.reasons).toEqual([
      "not_executed_streak",
      "escalation_l3",
      "completion_drop",
    ]);
    expect(j.not_executed_days).toBe(3);
    expect(j.consecutive_problem_days).toBe(4);
    expect(j.completion_rate).toBe(50);
    expect(j.previous_completion_rate).toBe(90);
    expect(j.drop_ratio).toBeCloseTo((90 - 50) / 90, 10);
  });

  it("★理由は識別子で返す（日本語を作らない。D-111）★", () => {
    const j = judgeRescue({ rows: notExecuted3Days, consecutive_problem_days: 5 });
    for (const reason of j.reasons) {
      // ★和文が混ざっていないこと★ — 混ざると EN 切替で日本語が残る
      expect(reason).toMatch(/^[a-z0-9_]+$/);
    }
  });

  it("★空配列なら required は false（型の約束。指示書AI §5-3）★", () => {
    const j = judgeRescue({ rows: [], consecutive_problem_days: 0 });
    expect(j.reasons).toEqual([]);
    expect(j.required).toBe(false);
    expect(j.completion_rate).toBeNull();
    expect(j.previous_completion_rate).toBeNull();
    expect(j.drop_ratio).toBeNull();
    expect(j.not_executed_days).toBe(0);
  });

  it("★付帯情報は丸めない生値（`CLAUDE.md` §4.3 / §9.3）★", () => {
    // 89.5% は表示すると 90% だが、判定にも付帯情報にも丸めた値を使わない
    const j = judgeRescue({
      rows: [done("2026-09-01", 100), done("2026-09-02", 89.5)],
      consecutive_problem_days: 0,
    });
    expect(j.completion_rate).toBe(89.5);
    expect(j.completion_rate).not.toBe(90);
  });

  it("★行は日付昇順でなくてよい（内部で並べ替える）★", () => {
    const shuffled = [
      done("2026-09-09", 72.5),
      done("2026-09-07", 94.7),
    ];
    const j = judgeRescue({ rows: shuffled, consecutive_problem_days: 0 });
    expect(j.completion_rate).toBeCloseTo(72.5, 10);
    expect(j.previous_completion_rate).toBeCloseTo(94.7, 10);
  });

  it("★DBアクセスを混ぜない純粋関数（同じ入力で必ず同じ結果）★", () => {
    const input = { rows: droppedRows, consecutive_problem_days: 3 };
    expect(judgeRescue(input)).toEqual(judgeRescue(input));
  });
});

// ---------------------------------------------------------------- 表6: 面積加重（旧）→ 1回ごとの完了率の平均（新）で結果が変わるケース

/**
 * ★表6: R-3 の判定が面積加重（旧）と1回ごとの完了率の単純平均（新）で変わるケース★
 *   （指示書AQ 決定4 / §5-1。2026-09-30）。
 * ★表の「clean_ratio」は判定に渡す1回ごとの完了率（`completion_rate`）のこと★（D-155 以降はデータ層が
 *   面積から `runCompletionRate()` で作る）。★判定は面積を読まず、渡された `completion_rate` だけを使う★ —
 *   面積は「付いたまま届いても読まない」ことの確認のために付けてある。
 *
 * ★★「判定結果が変わったこと」を仕様として固定する★★ — 面積比に戻すと、ここが落ちる。
 * ★数値の出所★: 6-1 / 6-2 は 3-8 / 3-9 と同じくユーザー提供CSV `clean_task_reports.csv`（189行）の
 *   個別行の乖離例（面積比 13.8% ↔ `clean_ratio` 100.0% / 面積比 162.8% ↔ `clean_ratio` 61.0%）を借りた作り値。
 *   6-3 / 6-4 は2台構成の施設を想定した作り値。
 *
 * | # | 前回（clean_ratio / 面積） | 当日（clean_ratio / 面積） | 旧: 面積比 | 新: clean_ratio 平均 |
 * |---|---|---|---|---|
 * | 6-1 | 80 / 1628÷1000 | 80 / 1000÷1000 | 162.8 → 100 ＝ 38.6%低下 ★急落★ | 80 → 80 ＝ 0% ★急落しない★ |
 * | 6-2 | 100 / 138÷1000 | 61 / 1628÷1000 | 13.8 → 162.8 ＝ 上昇 ★急落しない★ | 100 → 61 ＝ 39%低下 ★急落★ |
 * | 6-3 | 90 / 900÷1000 | 90 / 900÷1000 ＋ 50 / 10÷100 | 90 → 82.7 ＝ 8.1%低下 ★急落しない★ | 90 → 70 ＝ 22.2%低下 ★急落★ |
 * | 6-4 | 90 / 900÷1000 | 60 / 600÷1000 ＋ 100 / 100÷100 | 90 → 63.6 ＝ 29.3%低下 ★急落★ | 90 → 80 ＝ 11.1%低下 ★急落しない★ |
 */
describe("表6: 面積比と1回ごとの完了率の平均で R-3 の結果が変わるケース", () => {
  const PREV = "2026-09-01";
  const TODAY = "2026-09-02";
  const judge = (rows: readonly RescueRowInput[]) =>
    judgeRescue({ rows, consecutive_problem_days: 0 });

  it("★6-1: 面積比では急落（162.8→100）だが、clean_ratio は 80→80 なので急落しない★", () => {
    const j = judge([
      doneWithArea(PREV, 80, 1628, 1000),
      doneWithArea(TODAY, 80, 1000, 1000),
    ]);
    expect(j.previous_completion_rate).toBe(80);
    expect(j.completion_rate).toBe(80);
    expect(j.drop_ratio).toBe(0);
    expect(j.reasons).not.toContain("completion_drop");
    expect(j.required).toBe(false);
  });

  it("★6-2: 面積比では上昇（13.8→162.8）だが、clean_ratio は 100→61 で急落★", () => {
    const j = judge([
      doneWithArea(PREV, 100, 138, 1000),
      doneWithArea(TODAY, 61, 1628, 1000),
    ]);
    expect(j.previous_completion_rate).toBe(100);
    expect(j.completion_rate).toBe(61);
    expect(j.drop_ratio).toBeCloseTo(0.39, 10);
    expect(j.reasons).toEqual(["completion_drop"]);
  });

  it("★6-3: 小面積の台が低率 — 面積加重では薄まって急落しないが、単純平均では急落★", () => {
    const j = judge([
      doneWithArea(PREV, 90, 900, 1000),
      doneWithArea(TODAY, 90, 900, 1000),
      doneWithArea(TODAY, 50, 10, 100, "03:00:00"),
    ]);
    expect(j.completion_rate).toBe(70);
    expect(j.drop_ratio).toBeCloseTo((90 - 70) / 90, 10);
    expect(j.reasons).toEqual(["completion_drop"]);
  });

  it("★6-4: 大面積の台が低率 — 面積加重では急落だが、単純平均では急落しない★", () => {
    const j = judge([
      doneWithArea(PREV, 90, 900, 1000),
      doneWithArea(TODAY, 60, 600, 1000),
      doneWithArea(TODAY, 100, 100, 100, "03:00:00"),
    ]);
    expect(j.completion_rate).toBe(80);
    expect(j.drop_ratio).toBeCloseTo((90 - 80) / 90, 10);
    // ★閾値は設定値から読む（ハードコードしない）★
    expect(j.drop_ratio as number).toBeLessThan(COMPLETION_DROP.dropRatio);
    expect(j.reasons).not.toContain("completion_drop");
  });
});
