import { describe, expect, it } from "vitest";
import {
  CLEAN_TASK_REPORT_ID_DIGITS,
  CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH,
  CLEAN_TASK_REPORT_STATUS_DISTRIBUTION,
  cleanTaskReportCleanArea,
  cleanTaskReportId,
  cleanTaskReportNullTimetableLogIds,
  cleanTaskReportStatus,
  cleanTaskReportTaskArea,
  cleanTaskReportZeroTimetableRows,
  cleanTaskReports,
} from "@/lib/fixtures/clean-task-reports";
import { scheduledTimetableId } from "@/lib/db/timetable-link";
import { runCompletionRate } from "@/lib/judge";
import { boundaryCases, cleanLogs, escalationCases, statusCases } from "@/lib/fixtures/clean-logs";

/**
 * `clean_task_reports` のサンプルデータ（docs/fixtures.md §2.6）が
 * ★狙ったケースになっているか★ を固定するテスト。
 *
 * ★「落ちない」ことと「狙った数字になっている」ことは別★なのでここで固定する
 * （`lib/fixtures/__tests__/areas.test.ts` と同じ考え方）。
 */

describe("fixtures: cleanTaskReports", () => {
  it("未実施（completion_rate が null）の清掃には行を作らない（指示書AQ 1-1）", () => {
    const notExecuted = cleanLogs.filter((log) => log.completion_rate === null);
    // ★未実施のケースが fixtures から消えていないこと★（消えるとこのテストが無意味になる）
    expect(notExecuted.length).toBeGreaterThan(0);

    const reportedLogIds = new Set(
      cleanTaskReports.map((report) => report.clean_timetable_id)
    );
    for (const log of notExecuted) {
      expect(reportedLogIds.has(log.id), `未実施の id=${log.id} に行がある`).toBe(false);
    }
  });

  it("実施済みの清掃はすべて1行持つ（clean_timetable_id を NULL にした行も含む）", () => {
    const executed = cleanLogs.filter((log) => log.completion_rate !== null);
    expect(cleanTaskReports).toHaveLength(executed.length);
  });

  it("report_id は19桁の数値文字列で重複しない（varchar(20)）", () => {
    for (const report of cleanTaskReports) {
      expect(report.report_id).toMatch(/^\d+$/);
      expect(report.report_id).toHaveLength(CLEAN_TASK_REPORT_ID_DIGITS);
    }
    expect(new Set(cleanTaskReports.map((r) => r.report_id)).size).toBe(
      cleanTaskReports.length
    );
  });

  it("clean_ratio は対応する清掃ログの completion_rate と同じ（境界値10ケースを壊さない）", () => {
    const byId = new Map(cleanLogs.map((log) => [log.id, log]));
    for (const report of cleanTaskReports) {
      // ★NULL にした行は clean_timetable_id から引けない★ので report_id 側から引く
      const logId = report.clean_timetable_id;
      if (logId === null) continue;
      expect(report.clean_ratio).toBe(byId.get(logId)?.completion_rate);
    }
  });

  it("docs/fixtures.md §2.1 の境界値がすべて clean_ratio として存在する", () => {
    const ratios = new Set(cleanTaskReports.map((r) => r.clean_ratio));
    // ★未実施(null) は「行が無い」ことで表すので、ここには出てこない（上の1件目のテストが担保）★
    for (const value of [90.1, 90.0, 89.5, 70.1, 70.0, 10.1, 10.0, 9.9, 0]) {
      expect(ratios.has(value), `完了率 ${value} の行が無い`).toBe(true);
    }
  });

  it("status は実測の4種（3 / 4 / 5 / 6）がすべて出る", () => {
    const seen = new Set(cleanTaskReports.map((r) => r.status));
    for (const { status } of CLEAN_TASK_REPORT_STATUS_DISTRIBUTION) {
      expect(seen.has(status), `status=${status} の行が無い`).toBe(true);
    }
    expect([...seen].sort((a, b) => a - b)).toEqual([3, 4, 5, 6]);
  });

  it("status の巡回列はCSV実測189行の分布そのもの（3:2 / 4:161 / 5:4 / 6:22）", () => {
    expect(CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH).toBe(189);
    const counts: Record<number, number> = {};
    for (let i = 0; i < CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH; i += 1) {
      const status = cleanTaskReportStatus(i);
      counts[status] = (counts[status] ?? 0) + 1;
    }
    for (const { status, count } of CLEAN_TASK_REPORT_STATUS_DISTRIBUTION) {
      expect(counts[status], `status=${status}`).toBe(count);
    }
  });

  it("status の巡回列は塊にならず散っている（先頭189件が全部 4 にならない）", () => {
    const firstTwenty = Array.from({ length: 20 }, (_, i) => cleanTaskReportStatus(i));
    expect(new Set(firstTwenty).size).toBeGreaterThan(1);
  });

  it("ended_at が null の行がある（落とし穴3。順序キーは ended_at ?? started_at）", () => {
    const nullEnded = cleanTaskReports.filter((r) => r.ended_at === null);
    expect(nullEnded.length).toBeGreaterThan(0);
    // ★ended_at が null でも clean_ratio は入っている★（判定は壊れない）
    for (const report of nullEnded) {
      expect(report.clean_ratio).not.toBeNull();
      // ★終了時刻が無い行の clean_time は 0★
      expect(report.clean_time).toBe(0);
    }
  });

  it("clean_timetable_id が null の行がある（落とし穴1。fail-silent の検証用）", () => {
    const nullTimetable = cleanTaskReports.filter((r) => r.clean_timetable_id === null);
    expect(nullTimetable.length).toBeGreaterThan(0);
    expect(nullTimetable).toHaveLength(cleanTaskReportNullTimetableLogIds.size);
  });

  it("★境界値・エスカレーション・ステータスのケースには clean_timetable_id の NULL を当てない★", () => {
    // NULL にすると「未実施」として集計され、§2.1 / §2.2 のケースが画面で確認できなくなる
    const protectedIds = [...boundaryCases, ...escalationCases, ...statusCases].map(
      (log) => log.id
    );
    for (const id of protectedIds) {
      expect(
        cleanTaskReportNullTimetableLogIds.has(id),
        `id=${id}（境界値/エスカレーション/ステータスのケース）が NULL 対象になっている`
      ).toBe(false);
    }
  });

  it("clean_time が 0 の行がある（実測に 0 の行がある）", () => {
    expect(cleanTaskReports.some((r) => r.clean_time === 0)).toBe(true);
    expect(cleanTaskReports.some((r) => r.clean_time > 0)).toBe(true);
  });

  it("task_area は清掃エリア（section_id）ごとに一定", () => {
    const bySection = new Map<number, Set<number>>();
    for (const report of cleanTaskReports) {
      const set = bySection.get(report.section_id) ?? new Set<number>();
      set.add(report.task_area);
      bySection.set(report.section_id, set);
    }
    expect(bySection.size).toBeGreaterThan(1);
    for (const [sectionId, values] of bySection) {
      expect(values.size, `section_id=${sectionId} の task_area が一定でない`).toBe(1);
      expect([...values][0]).toBe(cleanTaskReportTaskArea(sectionId));
    }
  });

  it("task_area は清掃エリアごとに違う値になる（全エリア一律にしない）", () => {
    const areas = new Set(cleanTaskReports.map((r) => r.task_area));
    expect(areas.size).toBeGreaterThan(1);
  });

  // ★★D-155（指示書AS 段階6-1）: 完了率は面積から `runCompletionRate()` で決まる★★ —
  //   ★旧: 「過半数の行で task_area を超える（実測 68.3%）」「clean_ratio は面積比と一致しない」
  //   「面積比100%超で clean_ratio が低い行がある」の3件は、いまの仕様と逆なので削除した★
  it("★全行で runCompletionRate(row) が対応する清掃ログの completion_rate と一致する★（D-155）", () => {
    // ★`clean_timetable_id` を NULL にした行も含めるため `report_id`（元の清掃 id から作る）で引く★
    const byReportId = new Map(cleanLogs.map((log) => [cleanTaskReportId(log.id), log]));
    expect(cleanTaskReports.length).toBeGreaterThan(0);
    for (const report of cleanTaskReports) {
      const log = byReportId.get(report.report_id);
      expect(log, `report_id=${report.report_id} の清掃ログが無い`).toBeDefined();
      expect(runCompletionRate(report), `report_id=${report.report_id}`).toBe(log?.completion_rate);
    }
  });

  it("clean_area は変動する（エリア・完了率ごとに違う値）", () => {
    expect(new Set(cleanTaskReports.map((r) => r.clean_area)).size).toBeGreaterThan(1);
  });

  it("★完了率100の清掃は task_area を超える面積になる（上限100の確認）★", () => {
    // ★fixtures の清掃ログには完了率100が無い（最大 99.5）ので、関数を直接確かめる★
    const taskArea = 878.524992;
    for (const id of [1, 2, 3, 101, 999]) {
      const cleanArea = cleanTaskReportCleanArea(taskArea, 100, id);
      expect(cleanArea).toBeGreaterThanOrEqual(taskArea);
      expect(cleanArea).toBeLessThanOrEqual(taskArea * 1.4 + 1e-6);
      expect(runCompletionRate({ clean_area: cleanArea, task_area: taskArea })).toBe(100);
    }
  });

  it("started_at / ended_at は JST の ISO 8601（+09:00）", () => {
    for (const report of cleanTaskReports) {
      expect(report.started_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$/);
      if (report.ended_at !== null) {
        expect(report.ended_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$/);
        expect(new Date(report.ended_at).getTime()).toBeGreaterThan(
          new Date(report.started_at).getTime()
        );
      }
    }
  });

  it("section_id と robot_id は同値（P1は1清掃エリア=1ロボット）", () => {
    for (const report of cleanTaskReports) {
      expect(report.section_id).toBe(report.robot_id);
    }
  });
});

/**
 * ★`clean_timetable_id = 0` の行（予定外のタスク。TeamWorks 回答 2026-10-27）★。
 * ★清掃ログ由来の `cleanTaskReports` には入れず、別の配列で足してある★（既存の行・期待値を動かさないため）。
 */
describe("fixtures: cleanTaskReportZeroTimetableRows（clean_timetable_id = 0）", () => {
  it("1行以上あり、すべて clean_timetable_id が 0（NULL ではない）で、予定外と判定される", () => {
    expect(cleanTaskReportZeroTimetableRows.length).toBeGreaterThan(0);
    for (const report of cleanTaskReportZeroTimetableRows) {
      expect(report.clean_timetable_id).toBe(0);
      expect(scheduledTimetableId(report.clean_timetable_id)).toBeNull();
    }
  });

  it("report_id は19桁で、清掃ログ由来の行と重ならない", () => {
    const existing = new Set(cleanTaskReports.map((r) => r.report_id));
    for (const report of cleanTaskReportZeroTimetableRows) {
      expect(report.report_id).toMatch(/^\d+$/);
      expect(report.report_id).toHaveLength(CLEAN_TASK_REPORT_ID_DIGITS);
      expect(existing.has(report.report_id)).toBe(false);
    }
  });

  it("清掃ログ由来の cleanTaskReports には含めない（実施済みの清掃1回につき1行を崩さない）", () => {
    expect(cleanTaskReports.some((r) => r.clean_timetable_id === 0)).toBe(false);
  });

  it("面積は正の値・section_id と robot_id は同値・started_at は JST の ISO 8601", () => {
    for (const report of cleanTaskReportZeroTimetableRows) {
      expect(report.task_area).toBeGreaterThan(0);
      expect(report.clean_area).toBeGreaterThan(0);
      expect(report.section_id).toBe(report.robot_id);
      expect(report.started_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$/);
    }
  });
});
