import { describe, expect, it } from "vitest";
import {
  compareCleanLogItems,
  consecutiveProblemDaysByFacilityDate,
  facilityDateKey,
  isUnscheduledReport,
  scheduledCleanLogRowKey,
  unscheduledCleanLogItem,
  unscheduledCleanLogRowKey,
  type UnscheduledReportLike,
  type UnscheduledRowContext,
} from "@/lib/db/clean-log-mapping";
import {
  unscheduledCleanLogRowsFromReports,
  unscheduledCleanLogWhere,
  type UnscheduledReportRow,
} from "@/lib/db/unscheduled-clean-log-source";
import { FACILITY_SECTION_VISIBLE_WHERE } from "@/lib/db/visibility";
import type { CleanLogItem } from "@/lib/types";

/**
 * 清掃詳細ログの予定外タスクの行（指示書BG §1・§2-1 / 表U1）— 純粋関数の部分。
 * ルートを通した確認は `app/api/clean-logs/__tests__/route.test.ts` の「予定外タスクの行（指示書BG 表U1）」。
 *
 * 前提（表U1）: 今 = 2026-10-29 10:00 JST。施設 F1・清掃エリア S1。
 *
 * | # | 行 | 期待 |
 * |---|---|---|
 * | U1-1 | 予定外・`started_at` 10-28 21:00・完了率 95.9 | `unscheduled` true・`id` null・`row_key` "rp-…"・`departed` true・◎・事前チェック null |
 * | U1-2 | 予定外・`clean_area` 0 | 完了率 0・× ・出発 true（未実施ではない） |
 * | U1-3 | `clean_timetable_id` = 0 と NULL | どちらも予定外として同じ扱い |
 * | U1-4 | 予定外・`task_area` 0 | 完了率 0（今の `runCompletionRate()` と同じ） |
 * | U1-5 | 予定外・削除済みの施設 | 出さない・数えない（SQL の `where` の可視性。ルートのテストで行も確かめる） |
 * | U1-6 | 予定のある行 | 今と同じ（`unscheduled` false・`row_key` "tt-<id>"） |
 */

const F1: UnscheduledRowContext = {
  facility_id: 1,
  facility_name: "テスト施設F1",
  model_name: "PUDU CC1",
  serial_number: "S1-ROBOT",
};

function report(overrides: Partial<UnscheduledReportLike> = {}): UnscheduledReportLike {
  return {
    report_id: "9200000000000000001",
    clean_timetable_id: null,
    status: 4,
    task_area: 1000,
    clean_area: 959,
    clean_ratio: 95.9,
    started_at_iso: "2026-10-28T21:00:00+09:00",
    ...overrides,
  };
}

describe("unscheduledCleanLogItem — 表U1", () => {
  it("U1-1: 予定外・started_at 10-28 21:00・完了率 95.9 → 1行の中身", () => {
    expect(unscheduledCleanLogItem(report(), F1, 0)).toEqual<CleanLogItem>({
      id: null,
      row_key: "rp-9200000000000000001",
      report_id: "9200000000000000001",
      unscheduled: true,
      facility_id: 1,
      facility_name: "テスト施設F1",
      model_name: "PUDU CC1",
      serial_number: "S1-ROBOT",
      scheduled_at: "2026-10-28T21:00:00+09:00",
      status_1h: null,
      status_1h_minutes: null,
      status_30m: null,
      status_30m_minutes: null,
      departed: true,
      task_status: 4,
      result_pending: false,
      completion_rate: 95.9,
      grade: "◎",
      consecutive_problem_days: 0,
      escalation_level: null,
    });
  });

  it("U1-2: clean_area 0 → 完了率 0・×・出発 true（未実施ではない）", () => {
    const item = unscheduledCleanLogItem(report({ clean_area: 0, status: 6 }), F1, 1);
    expect([item.completion_rate, item.grade, item.departed, item.task_status]).toEqual([0, "×", true, 6]);
    expect(item.escalation_level).toBe("L1");
  });

  it("U1-3: clean_timetable_id が 0 と NULL は同じ扱い（どちらも予定外）", () => {
    const fromNull = unscheduledCleanLogItem(report({ clean_timetable_id: null }), F1, 0);
    const fromZero = unscheduledCleanLogItem(report({ clean_timetable_id: BigInt(0) }), F1, 0);
    const fromNumberZero = unscheduledCleanLogItem(report({ clean_timetable_id: 0 }), F1, 0);
    expect(fromZero).toEqual(fromNull);
    expect(fromNumberZero).toEqual(fromNull);
    expect(isUnscheduledReport({ clean_timetable_id: null })).toBe(true);
    expect(isUnscheduledReport({ clean_timetable_id: BigInt(0) })).toBe(true);
    expect(isUnscheduledReport({ clean_timetable_id: BigInt(103) })).toBe(false);
  });

  it("U1-3: 予定の id を持つレポートを渡したら例外（予定外の行に化けさせない）", () => {
    expect(() => unscheduledCleanLogItem(report({ clean_timetable_id: BigInt(103) }), F1, 0)).toThrow();
  });

  it("U1-4: task_area 0 → 完了率 0", () => {
    const item = unscheduledCleanLogItem(report({ task_area: 0, clean_area: 300 }), F1, 0);
    expect([item.completion_rate, item.grade, item.departed]).toEqual([0, "×", true]);
  });

  it("U1-6: 予定のある行のキーは tt-<id>、予定外は rp-<report_id>（同じ数字でも重ならない）", () => {
    expect(scheduledCleanLogRowKey(103)).toBe("tt-103");
    expect(scheduledCleanLogRowKey(BigInt(103))).toBe("tt-103");
    expect(unscheduledCleanLogRowKey("103")).toBe("rp-103");
  });
});

describe("unscheduledCleanLogWhere — 予定外の行の SQL の条件", () => {
  it("予定外（0・NULL）・削除済みを除く・started_at の期間（JST の壁時計値）・施設", () => {
    const where = unscheduledCleanLogWhere({
      id: null,
      date_from: "2026-10-28",
      date_to: "2026-10-28",
      facility_ids: [1, 2],
    });
    expect(where).toEqual({
      OR: [{ clean_timetable_id: null }, { clean_timetable_id: BigInt(0) }],
      ...FACILITY_SECTION_VISIBLE_WHERE,
      // ★壁時計値（`Z` を付けた JST の 00:00）★ — `dbDateRange()` と同じ
      started_at: {
        gte: new Date("2026-10-28T00:00:00Z"),
        lt: new Date("2026-10-29T00:00:00Z"),
      },
      facility_id: { in: [BigInt(1), BigInt(2)] },
    });
  });

  it("U1-5: 削除済みの施設・清掃エリアを除く条件が必ず付く（期間・施設の指定が無くても）", () => {
    const where = unscheduledCleanLogWhere({ id: null, date_from: null, date_to: null, facility_ids: null });
    expect(where).toMatchObject(FACILITY_SECTION_VISIBLE_WHERE);
    expect(where).not.toHaveProperty("started_at");
    expect(where).not.toHaveProperty("facility_id");
  });

  it("★罠1★ facility_ids が [] なら in: []（0件）。null と混同しない", () => {
    const where = unscheduledCleanLogWhere({ id: null, date_from: null, date_to: null, facility_ids: [] });
    expect(where.facility_id).toEqual({ in: [] });
  });
});

describe("unscheduledCleanLogRowsFromReports — 読んだ行の組み立て", () => {
  function row(overrides: Partial<UnscheduledReportRow> = {}): UnscheduledReportRow {
    return {
      report_id: "9200000000000000001",
      facility_id: BigInt(1),
      section_id: BigInt(10),
      clean_timetable_id: null,
      status: 4,
      task_area: 1000,
      clean_area: 959,
      clean_ratio: 95.9,
      // ★DB の壁時計値（JST 21:00 を `Z` で持つ）★
      started_at: new Date("2026-10-28T21:00:00Z"),
      facility: { facility_name: "テスト施設F1" },
      section: { robots: [{ serial_number: "S1-ROBOT", device: { maker_id: 1, model_name: "CC1" } }] },
      ...overrides,
    };
  }

  it("started_at は jstIsoFromDb() で JST の ISO8601（9時間を足さない）・機種はメーカー名つき", () => {
    const [r] = unscheduledCleanLogRowsFromReports([row()]);
    expect(r.report.started_at_iso).toBe("2026-10-28T21:00:00+09:00");
    expect(r.context).toEqual(F1);
    expect(r.robot_count).toBe(1);
  });

  it("予定の id を持つ行は落とす（SQL の写しが外れていても混ぜない）・0 と NULL は残す", () => {
    const rows = unscheduledCleanLogRowsFromReports([
      row({ report_id: "a", clean_timetable_id: null }),
      row({ report_id: "b", clean_timetable_id: BigInt(0) }),
      row({ report_id: "c", clean_timetable_id: BigInt(103) }),
    ]);
    expect(rows.map((r) => r.report.report_id)).toEqual(["a", "b"]);
  });

  it("清掃エリアに有効なロボットがいなければ機種・シリアルは null（予定のある行と同じ）", () => {
    const [r] = unscheduledCleanLogRowsFromReports([row({ section: { robots: [] } })]);
    expect([r.context.model_name, r.context.serial_number, r.robot_count]).toEqual([null, null, 0]);
  });
});

describe("compareCleanLogItems — 予定のある行と予定外の行を混ぜて並べる", () => {
  const base = unscheduledCleanLogItem(report(), F1, 0);
  const scheduled = (id: number, at: string): CleanLogItem => ({
    ...base,
    id,
    row_key: scheduledCleanLogRowKey(id),
    report_id: null,
    unscheduled: false,
    scheduled_at: at,
  });
  const unscheduled = (reportId: string, at: string): CleanLogItem => ({
    ...base,
    row_key: unscheduledCleanLogRowKey(reportId),
    report_id: reportId,
    scheduled_at: at,
  });

  it("scheduled_at の新しい順。同着は予定のある行（id 昇順）→ 予定外（row_key 昇順）", () => {
    const rows = [
      unscheduled("2", "2026-10-28T21:00:00+09:00"),
      scheduled(5, "2026-10-28T21:00:00+09:00"),
      unscheduled("1", "2026-10-28T21:00:00+09:00"),
      scheduled(3, "2026-10-28T21:00:00+09:00"),
      unscheduled("9", "2026-10-28T23:00:00+09:00"),
      scheduled(1, "2026-10-27T21:00:00+09:00"),
    ];
    expect(rows.sort(compareCleanLogItems).map((r) => r.row_key)).toEqual([
      "rp-9",
      "tt-3",
      "tt-5",
      "rp-1",
      "rp-2",
      "tt-1",
    ]);
  });
});

describe("consecutiveProblemDaysByFacilityDate — 予定外の行の連続日数を施設 × 日付で引く", () => {
  const input = (
    id: number,
    facilityId: number,
    date: string,
    rate: number | null
  ) => ({
    id,
    unscheduled: false,
    facility_id: facilityId,
    scheduled_at: `${date}T21:00:00+09:00`,
    completion_rate: rate,
    departed: rate !== null,
    clean_area: 0,
    task_area: 0,
  });

  it("同じ施設の同じ日は全行同じ値（id で引く値と一致する）。施設ごとに独立", () => {
    const rows = [
      input(1, 1, "2026-10-26", 5),
      input(2, 1, "2026-10-27", 50),
      input(3, 1, "2026-10-28", 95),
      input(4, 1, "2026-10-28", 30),
      input(5, 2, "2026-10-28", 95),
    ];
    const map = consecutiveProblemDaysByFacilityDate(rows);
    expect(map.get(facilityDateKey(1, "2026-10-28T23:00:00+09:00"))).toBe(3);
    expect(map.get(facilityDateKey(1, "2026-10-27"))).toBe(2);
    expect(map.get(facilityDateKey(2, "2026-10-28"))).toBe(0);
    expect(map.has(facilityDateKey(1, "2026-10-29"))).toBe(false);
  });
});
