import { describe, expect, it } from "vitest";
import {
  buildAreaCompletions,
  overallCompletionTally,
  parseAreaFilter,
  selectAreaRowsForLog,
} from "@/lib/clean-logs/areas";
import type { AreaCompletionRow } from "@/lib/types";

/**
 * エリア別完了率（GET /clean-logs/{id}/areas）の期待値表テスト
 *
 * ★仕様の正★: `d-watch-api.yaml` の `/clean-logs/{id}/areas` / `CLAUDE.md` §4.1 §4.2 §4.3 /
 *   指示書Z（`docs/handoff/指示書Z_エリア別完了率を実装する.md`）の決定3〜決定13。
 *
 * ★この表を変えたくなったら、先に YAML と CLAUDE.md を確認すること★
 *   （実装に合わせて期待値を書き換えない。D-041）。
 *
 * ★★とくに B のケース（面積が違う2台）を消さないこと★★ —
 *   面積が同じデータだけだと、★面積加重に戻してもテストが緑のまま通ってしまう★。
 *
 * ★★2026-09-30: 全体率を面積比から `clean_ratio` 列の単純平均に変えた★★（指示書AQ 決定4 / §5-1）—
 *   ★2026-10-02（D-155 / 指示書AS）: 平均する材料は `clean_ratio` 列から1回ごとの完了率（`runCompletionRate()`）に変わった。
 *   このテストの行の `completion_rate` がその値で、表の期待値は変わらない★
 *   ★レポート画面・F-08・R-3 と同じ `meanCleanRatio()`（`lib/judge/kpi.ts`）で畳む★。
 *   ★B / C の期待値はこのとき書き換えた★（ユーザー指示 2026-09-30。実装に合わせた書き換えではない）。
 */

/** 期待値表を読みやすくするための行ビルダ（`clean_area` は完了率と整合させる） */
function row(input: {
  section_id: number;
  completion_rate: number | null;
  task_area?: number;
  clean_area?: number;
  facility_id?: number;
  date?: string;
  clean_log_id?: number;
}): AreaCompletionRow {
  const taskArea = input.task_area ?? 0;
  return {
    clean_log_id: input.clean_log_id ?? 9000 + input.section_id,
    facility_id: input.facility_id ?? 7,
    date: input.date ?? "2026-09-10",
    section_id: input.section_id,
    section_name: `清掃エリア（ロボット${input.section_id}）`,
    completion_rate: input.completion_rate,
    task_area: taskArea,
    clean_area:
      input.clean_area ??
      (input.completion_rate === null
        ? 0
        : (taskArea * input.completion_rate) / 100),
  };
}

// ---------------------------------------------------------------------------
// A. 並び順と絞り込み
//
//   section_id  completion_rate  期待 grade
//   101         95.0             ◎
//   102         42.5             △
//   103         null（未実施）    ×
//   104          5.0             ×
//   105         70.0             ★○★  ← 「70%以上」が○なので 70.0 は ○（D-139。旧規則では △）
//
//   filter=all     の並び: 103(null) → 104(5.0) → 102(42.5) → 105(70.0) → 101(95.0)
//   filter=problem の並び: 103 → 104 → 102（★105 は ○ になったので落ちる★ / 101 も落ちる）
//   total_count   : どちらの filter でも 5
//   problem_count : どちらの filter でも ★3★（D-139 以前は 4）
// ---------------------------------------------------------------------------
const caseA: AreaCompletionRow[] = [
  row({ section_id: 101, completion_rate: 95.0, task_area: 1000 }),
  row({ section_id: 102, completion_rate: 42.5, task_area: 1000 }),
  row({ section_id: 103, completion_rate: null, task_area: 0 }),
  row({ section_id: 104, completion_rate: 5.0, task_area: 1000 }),
  row({ section_id: 105, completion_rate: 70.0, task_area: 1000 }),
];

describe("buildAreaCompletions — A. 並び順と絞り込み", () => {
  it("filter=all は完了率の昇順・未実施(null)が先頭", () => {
    const res = buildAreaCompletions(caseA, "all");
    expect(res.items.map((i) => i.section_id)).toEqual([103, 104, 102, 105, 101]);
    expect(res.items.map((i) => i.grade)).toEqual(["×", "×", "△", "○", "◎"]);
  });

  it("filter=problem は △× だけを同じ並びで返す", () => {
    const res = buildAreaCompletions(caseA, "problem");
    // ★105（70.0）は D-139 で ○ になったので問題行から外れる★
    expect(res.items.map((i) => i.section_id)).toEqual([103, 104, 102]);
  });

  it("total_count / problem_count は filter の前に数える", () => {
    for (const filter of ["all", "problem"] as const) {
      const res = buildAreaCompletions(caseA, filter);
      expect(res.total_count).toBe(5);
      expect(res.problem_count).toBe(3);
    }
  });

  it("完了率が同値のときは section_id の昇順で安定する", () => {
    const rows = [
      row({ section_id: 202, completion_rate: 50 }),
      row({ section_id: 201, completion_rate: 50 }),
      row({ section_id: 204, completion_rate: null }),
      row({ section_id: 203, completion_rate: null }),
    ];
    const res = buildAreaCompletions(rows, "all");
    expect(res.items.map((i) => i.section_id)).toEqual([203, 204, 201, 202]);
  });

  it("completion_rate と section_name は生値をそのまま返す（丸めない・整形しない）", () => {
    const res = buildAreaCompletions(
      [row({ section_id: 107, completion_rate: 42.5, task_area: 1000 })],
      "all"
    );
    expect(res.items[0].completion_rate).toBe(42.5);
    expect(res.items[0].section_name).toBe("清掃エリア（ロボット107）");
  });

  it("★入力の配列を書き換えない（並べ替えは複製に対して行う）★", () => {
    const rows = [...caseA];
    buildAreaCompletions(rows, "all");
    expect(rows.map((r) => r.section_id)).toEqual([101, 102, 103, 104, 105]);
  });
});

// ---------------------------------------------------------------------------
// B. 全体率は ★各行の完了率（1回ごとの完了率）の単純平均（清掃1回＝1票）★（指示書AQ 決定4 / §5-1・D-155）
//
//   section_id  task_area  clean_area  completion_rate（行の完了率）
//   201         1000       500         50.0
//   202          100       100        100.0
//
//   単純平均（正）  : (50 + 100) ÷ 2   = 75.0        → ○
//   面積ベース（廃止）: 600 ÷ 1100 × 100 = 54.5454...  → △
// ---------------------------------------------------------------------------
describe("buildAreaCompletions — B. 全体率は行の完了率の単純平均", () => {
  const caseB: AreaCompletionRow[] = [
    row({ section_id: 201, completion_rate: 50.0, task_area: 1000, clean_area: 500 }),
    row({ section_id: 202, completion_rate: 100.0, task_area: 100, clean_area: 100 }),
  ];

  it("★行の完了率の単純平均で出す（面積ベース 54.5… にならない）★", () => {
    const res = buildAreaCompletions(caseB, "all");
    expect(res.overall_completion_rate).toBe(75);
    expect(res.overall_completion_rate).not.toBeCloseTo((600 / 1100) * 100, 5);
    expect(res.overall_grade).toBe("○");
  });

  it("filter=problem でも全体率は全エリアから計算する", () => {
    const problem = buildAreaCompletions(caseB, "problem");
    const all = buildAreaCompletions(caseB, "all");
    expect(problem.overall_completion_rate).toBe(all.overall_completion_rate);
    expect(problem.total_count).toBe(2);
    // 50.0 = △ だけが残る
    expect(problem.items.map((i) => i.section_id)).toEqual([201]);
  });

  it("★行も全体も面積から再計算しない（行の完了率が面積と食い違うデータ）★", () => {
    // ★以前は「行は clean_ratio・全体は面積」という意図した非対称だったが、解消した★
    const res = buildAreaCompletions(
      [row({ section_id: 301, completion_rate: 12.3, task_area: 1000, clean_area: 900 })],
      "all"
    );
    expect(res.items[0].completion_rate).toBe(12.3);
    expect(res.overall_completion_rate).toBe(12.3);
    // ★面積比（90）にならない★
    expect(res.overall_completion_rate).not.toBe(90);
  });

  it("★clean_area > task_area の行（CSV実測で 129/189 行）でも 100% を超えない★", () => {
    // ★数値の出所: ユーザー提供CSV `clean_task_reports.csv` の乖離例（面積比 162.8% ↔ clean_ratio 61.0%）★
    const res = buildAreaCompletions(
      [row({ section_id: 302, completion_rate: 61.0, task_area: 1000, clean_area: 1628 })],
      "all"
    );
    expect(res.overall_completion_rate).toBe(61);
    expect(res.overall_grade).toBe("△");
  });
});

// ---------------------------------------------------------------------------
// C. 全体率の境界と未実施（判定は丸める前の生値で行う。CLAUDE.md §4.3 / §9.4）
//
//   ケース            行の clean_ratio   overall_completion_rate  overall_grade
//   ちょうど90.0       [90.0]             90.0                    ★◎（90%以上が◎。D-139）★
//   90.0のすぐ上       [90.1]             90.1                    ◎
//   ちょうど70.0       [70.0]             70.0                    ★○（70%以上が○。D-139）★
//   70.0のすぐ上       [70.1]             70.1                    ○
//   ちょうど10.0       [10.0]             10.0                    △（10%未満が×）
//   10.0のすぐ下       [9.9]               9.9                    ×
//   0%                 [0]                 0                      ×
//   全エリア未実施      [null]             null                    ×
//   ★89.5（2台平均）★ [89, 90]           89.5                    ★○（表示は90%だが判定は○）★
//   ★90.4（1台）★     [90.4]             90.4                    ★◎（表示は90%で判定も◎）★
// ---------------------------------------------------------------------------
describe("buildAreaCompletions — C. 全体率の境界と未実施", () => {
  const table: Array<{
    name: string;
    rates: Array<number | null>;
    overall: number | null;
    grade: string;
  }> = [
    { name: "ちょうど90.0", rates: [90.0], overall: 90.0, grade: "◎" },
    { name: "90.0のすぐ上", rates: [90.1], overall: 90.1, grade: "◎" },
    { name: "ちょうど70.0", rates: [70.0], overall: 70.0, grade: "○" },
    { name: "70.0のすぐ上", rates: [70.1], overall: 70.1, grade: "○" },
    { name: "ちょうど10.0", rates: [10.0], overall: 10.0, grade: "△" },
    { name: "10.0のすぐ下", rates: [9.9], overall: 9.9, grade: "×" },
    { name: "0%", rates: [0], overall: 0, grade: "×" },
    { name: "全エリア未実施", rates: [null], overall: null, grade: "×" },
    { name: "89.5（2台平均。表示90%だが○）", rates: [89, 90], overall: 89.5, grade: "○" },
    { name: "90.4（表示90%で◎）", rates: [90.4], overall: 90.4, grade: "◎" },
  ];

  it.each(table)(
    "$name → overall=$overall / grade=$grade",
    ({ rates, overall, grade }) => {
      const res = buildAreaCompletions(
        rates.map((rate, i) =>
          row({
            section_id: 401 + i,
            completion_rate: rate,
            task_area: rate === null ? 0 : 1000,
          })
        ),
        "all"
      );
      if (overall === null) {
        expect(res.overall_completion_rate).toBeNull();
      } else {
        expect(res.overall_completion_rate).toBeCloseTo(overall, 10);
      }
      expect(res.overall_grade).toBe(grade);
    }
  );

  it("★未実施（null）を 0% と混ぜない★ — 全エリア未実施のときだけ overall は null", () => {
    const notExecuted = buildAreaCompletions(
      [row({ section_id: 501, completion_rate: null, task_area: 0 })],
      "all"
    );
    expect(notExecuted.items[0].completion_rate).toBeNull();
    expect(notExecuted.overall_completion_rate).toBeNull();

    const zeroPercent = buildAreaCompletions(
      [row({ section_id: 502, completion_rate: 0, task_area: 1000, clean_area: 0 })],
      "all"
    );
    expect(zeroPercent.items[0].completion_rate).toBe(0);
    expect(zeroPercent.overall_completion_rate).toBe(0);
  });

  it("★未実施のエリアは平均の分母に入れない★（同じ日の実施エリアだけで全体率が決まる）", () => {
    // 施設内に「レポート行なし（＝未実施）」と「正常終了」が混在する日。
    // ★0% として混ぜると 47.5 になる★（稼働率で既に数えている失敗を二重に数える）
    const res = buildAreaCompletions(
      [
        row({ section_id: 601, completion_rate: null, task_area: 0, clean_area: 0 }),
        row({ section_id: 602, completion_rate: 95.0, task_area: 1000, clean_area: 950 }),
      ],
      "all"
    );
    expect(res.overall_completion_rate).toBe(95);
    expect(res.overall_grade).toBe("◎");
    expect(res.total_count).toBe(2);
    expect(res.problem_count).toBe(1);
    expect(res.items.map((i) => i.section_id)).toEqual([601, 602]);
  });

  it("★未実施行に面積が残っていても分母に入れない★（面積は読まない）", () => {
    // ★DBの値をそのまま足す方針（決定13）なので、未実施でも面積が 0 とは限らない★
    const res = buildAreaCompletions(
      [
        row({ section_id: 701, completion_rate: null, task_area: 1000, clean_area: 0 }),
        row({ section_id: 702, completion_rate: 80.0, task_area: 1000, clean_area: 800 }),
      ],
      "all"
    );
    expect(res.overall_completion_rate).toBe(80);
  });
});

// ---------------------------------------------------------------------------
// D. エリア数
// ---------------------------------------------------------------------------
describe("buildAreaCompletions — D. エリア数", () => {
  it("1エリアだけの施設でも1件返す（エラーにしない）", () => {
    const res = buildAreaCompletions(
      [row({ section_id: 11, completion_rate: 88, task_area: 1000 })],
      "all"
    );
    expect(res.items).toHaveLength(1);
    expect(res.total_count).toBe(1);
  });

  it("3エリアの施設は3件返す", () => {
    const res = buildAreaCompletions(
      [
        row({ section_id: 21, completion_rate: 62, task_area: 1000 }),
        row({ section_id: 22, completion_rate: 94, task_area: 100 }),
        row({ section_id: 23, completion_rate: 88, task_area: 100 }),
      ],
      "all"
    );
    expect(res.items).toHaveLength(3);
    expect(res.total_count).toBe(3);
  });

  it("該当なし（全部◎○）のとき filter=problem は空配列。404 にしない", () => {
    const rows = [
      row({ section_id: 31, completion_rate: 95, task_area: 1000 }),
      row({ section_id: 32, completion_rate: 80, task_area: 1000 }),
    ];
    const res = buildAreaCompletions(rows, "problem");
    expect(res.items).toEqual([]);
    expect(res.total_count).toBe(2);
    expect(res.problem_count).toBe(0);
    expect(res.overall_completion_rate).toBeCloseTo(87.5, 10);
  });

  it("行が1件も無いとき（想定外だが落ちないこと）", () => {
    const res = buildAreaCompletions([], "all");
    expect(res.items).toEqual([]);
    expect(res.total_count).toBe(0);
    expect(res.problem_count).toBe(0);
    expect(res.overall_completion_rate).toBeNull();
    expect(res.overall_grade).toBe("×");
  });
});

// ---------------------------------------------------------------------------
// E. selectAreaRowsForLog（施設 × 日付で取り出す。★その清掃ログ自身を含む★）
// ---------------------------------------------------------------------------
describe("overallCompletionTally — 全体率の分子・分母（指示書AR 段階4）", () => {
  it("★分子は null でない行の完了率の合計、分母はその行数（未実施は数えない）★", () => {
    // caseA: 95.0 / 42.5 / null / 5.0 / 70.0 → 分子 212.5・分母 4
    expect(overallCompletionTally(caseA)).toEqual({ cleanRatioSum: 212.5, cleanRatioCount: 4 });
  });

  it("★`buildAreaCompletions()` の全体率 === 分子 ÷ 分母★（ログと画面が食い違わない）", () => {
    const tally = overallCompletionTally(caseA);
    expect(buildAreaCompletions(caseA, "problem").overall_completion_rate).toBe(
      tally.cleanRatioSum / tally.cleanRatioCount
    );
  });

  it("全行が未実施なら分母0（全体率は null）", () => {
    const rows = [row({ section_id: 1, completion_rate: null })];
    expect(overallCompletionTally(rows)).toEqual({ cleanRatioSum: 0, cleanRatioCount: 0 });
    expect(buildAreaCompletions(rows, "all").overall_completion_rate).toBeNull();
  });
});

describe("selectAreaRowsForLog", () => {
  const rows: AreaCompletionRow[] = [
    row({ section_id: 71, completion_rate: 62, facility_id: 7, date: "2026-09-10" }),
    row({ section_id: 72, completion_rate: 94, facility_id: 7, date: "2026-09-10" }),
    row({ section_id: 73, completion_rate: 88, facility_id: 7, date: "2026-09-09" }),
    row({ section_id: 51, completion_rate: 97, facility_id: 5, date: "2026-09-10" }),
  ];

  it("同じ施設・同じ日だけを返す（自分自身を含む）", () => {
    const picked = selectAreaRowsForLog(rows, {
      facility_id: 7,
      date: "2026-09-10",
    });
    expect(picked.map((r) => r.section_id)).toEqual([71, 72]);
  });

  it("同じ施設でも違う日は含まない", () => {
    const picked = selectAreaRowsForLog(rows, {
      facility_id: 7,
      date: "2026-09-09",
    });
    expect(picked.map((r) => r.section_id)).toEqual([73]);
  });

  it("同じ日でも違う施設は含まない", () => {
    const picked = selectAreaRowsForLog(rows, {
      facility_id: 5,
      date: "2026-09-10",
    });
    expect(picked.map((r) => r.section_id)).toEqual([51]);
  });

  it("該当が無ければ空配列", () => {
    expect(
      selectAreaRowsForLog(rows, { facility_id: 99, date: "2026-09-10" })
    ).toEqual([]);
  });
});

// ---------------------------------------------------------------------------
// F. parseAreaFilter（既定は problem。不正値は 422）
// ---------------------------------------------------------------------------
describe("parseAreaFilter", () => {
  function parse(qs: string) {
    return parseAreaFilter(new URLSearchParams(qs));
  }

  it("指定なしは problem（既定）", () => {
    const r = parse("");
    expect(r.ok).toBe(true);
    if (r.ok) expect(r.query).toBe("problem");
  });

  it.each(["problem", "all"] as const)("filter=%s はそのまま", (value) => {
    const r = parse(`filter=${value}`);
    expect(r.ok).toBe(true);
    if (r.ok) expect(r.query).toBe(value);
  });

  it("不正値は 422 validation_failed", () => {
    const r = parse("filter=critical");
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(422);
      expect(r.error.error.code).toBe("validation_failed");
      expect(r.error.error.details).toEqual({
        filter: ["problem / all のいずれかを指定してください"],
      });
    }
  });

  it("★空文字（filter=）も 422★ — parseCleanLogQuery の result と同じ扱いに揃える", () => {
    const r = parse("filter=");
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(422);
  });
});
