import { describe, expect, it } from "vitest";
import {
  filterDsAlertsByStatus,
  filterDsAlertsByType,
  parseDsAlertQuery,
  sortDsAlerts,
  summarizeDsAlerts,
} from "@/lib/ds-alerts/query";
import type { DsAlert } from "@/lib/types";

function row(overrides: Partial<DsAlert>): DsAlert {
  return {
    id: "1",
    alert_type: "security_off",
    device_serial: "DS-001",
    facility_id: 1,
    facility_name: "メガバリューマート中央本店",
    section_name: "1F売り場",
    detected_at: "2026-08-27T03:42:00+09:00",
    resolution_status: "unresolved",
    responded_at: null,
    resolved_at: null,
    note: null,
    ...overrides,
  };
}

describe("parseDsAlertQuery", () => {
  it("省略時は type=all, status=open になる", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams());
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) throw new Error("query");
    expect(parsed.query.type).toBe("all");
    expect(parsed.query.status).toBe("open");
  });

  it("typeが不正な値なら422", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ type: "bogus" }));
    expect(parsed.ok).toBe(false);
    if (!parsed.ok) {
      expect(parsed.status).toBe(422);
      expect(parsed.error.error.code).toBe("validation_failed");
    }
  });

  it("statusが不正な値なら422", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ status: "bogus" }));
    expect(parsed.ok).toBe(false);
    if (!parsed.ok) {
      expect(parsed.status).toBe(422);
      expect(parsed.error.error.code).toBe("validation_failed");
    }
  });

  // ★どのパラメータが不正か呼び出し側に伝わること（details）を固定する★
  it("type不正のdetailsは選択肢を列挙する", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ type: "bogus" }));
    expect(parsed.ok).toBe(false);
    if (!parsed.ok) {
      expect(parsed.error.error.message).toBe("typeの値が不正です");
      expect(parsed.error.error.details).toEqual({
        type: ["all / security_off / disconnected のいずれかを指定してください"],
      });
    }
  });

  it("status不正のdetailsは選択肢を列挙する", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ status: "bogus" }));
    expect(parsed.ok).toBe(false);
    if (!parsed.ok) {
      expect(parsed.error.error.message).toBe("statusの値が不正です");
      expect(parsed.error.error.details).toEqual({
        status: ["open / all / resolved のいずれかを指定してください"],
      });
    }
  });

  // ★指示書AX 表2 A2・A4（D10）: 旧 `done` は別名で受けない（黙って倒さない）★
  it("status=resolved は通る", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ status: "resolved" }));
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) throw new Error("query");
    expect(parsed.query.status).toBe("resolved");
  });

  it("★A4: status=done（旧の値）は422★", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ status: "done" }));
    expect(parsed.ok).toBe(false);
    if (parsed.ok) throw new Error("422を期待");
    expect(parsed.status).toBe(422);
    expect(parsed.error.error.details).toEqual({
      status: ["open / all / resolved のいずれかを指定してください"],
    });
  });

  it("★A5: type=security_off は422にしない（enum は残す。D4）★", () => {
    expect(parseDsAlertQuery(new URLSearchParams({ type: "security_off" })).ok).toBe(true);
  });

  /**
   * ★group_id / customer_id（指示書AK 段階2）★
   *
   * ★★元は「渡してもパース結果に含まれない（D-053）」だったテストを反転させたもの★★ —
   *   ★`it` ごと消さないこと★。1本も残っていないと、次に誰かが「受け取って捨てる」
   *   実装に戻しても気づけない（指示書AK 落とし穴8）。
   *
   * | # | 入力 | 期待 |
   * |---|---|---|
   * | 2-1 | `group_id=1` | `1`（★`toHaveProperty` ではなく値まで見る★） |
   * | 2-2 | `customer_id=2` | `2` |
   * | 2-3 | 未指定 | ★`null`★（絞らない） |
   * | 2-4 | `group_id=0` | ★422★（0 は「グループなし」のセンチネル。有効なIDではない。D-011） |
   * | 2-5 | `group_id=1abc` | ★422★（★`parseInt` の前方一致で `1` にしない★。D-123 で実際に踏んだ罠） |
   * | 2-6 | `group_id=-1` | 422 |
   * | 2-7 | `group_id=1.5` | 422 |
   * | 2-8 | `9007199254740992`（安全整数超） | 422 |
   */
  it("group_id / customer_id を値として読む（D-053 解除。指示書AK 段階2）", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ group_id: "1", customer_id: "2" }));
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) throw new Error("query");
    expect(parsed.query.groupId).toBe(1);
    expect(parsed.query.customerId).toBe(2);
  });

  it("group_id / customer_id 未指定は null（絞らない）", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams());
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) throw new Error("query");
    expect(parsed.query.groupId).toBeNull();
    expect(parsed.query.customerId).toBeNull();
  });

  it.each(["0", "1abc", "-1", "1.5", "9007199254740992"])(
    "group_id=%s は422",
    (value) => {
      const parsed = parseDsAlertQuery(new URLSearchParams({ group_id: value }));
      expect(parsed.ok).toBe(false);
      if (parsed.ok) throw new Error("422を期待");
      expect(parsed.status).toBe(422);
      expect(parsed.error.error.details).toEqual({
        group_id: ["1以上の整数で指定してください"],
      });
    }
  );

  it.each(["0", "1abc"])("customer_id=%s も同じ規則で422", (value) => {
    const parsed = parseDsAlertQuery(new URLSearchParams({ customer_id: value }));
    expect(parsed.ok).toBe(false);
    if (parsed.ok) throw new Error("422を期待");
    expect(parsed.status).toBe(422);
    expect(parsed.error.error.details).toEqual({
      customer_id: ["1以上の整数で指定してください"],
    });
  });

  it("page/per_pageの既定値と上限", () => {
    const parsed = parseDsAlertQuery(new URLSearchParams());
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) throw new Error("query");
    expect(parsed.query.page).toBe(1);
    expect(parsed.query.perPage).toBe(50);

    const capped = parseDsAlertQuery(new URLSearchParams({ per_page: "9999" }));
    expect(capped.ok).toBe(true);
    if (!capped.ok) throw new Error("query");
    expect(capped.query.perPage).toBe(200);
  });
});

describe("filterDsAlertsByType", () => {
  const items: DsAlert[] = [
    row({ id: "1", alert_type: "security_off" }),
    row({ id: "2", alert_type: "disconnected" }),
  ];

  it("type=all は全件保持する", () => {
    expect(filterDsAlertsByType(items, "all")).toHaveLength(2);
  });

  it("type=security_off で絞り込む", () => {
    expect(filterDsAlertsByType(items, "security_off").map((a) => a.id)).toEqual(["1"]);
  });

  it("type=disconnected で絞り込む", () => {
    expect(filterDsAlertsByType(items, "disconnected").map((a) => a.id)).toEqual(["2"]);
  });
});

describe("filterDsAlertsByStatus（指示書AX D10）", () => {
  const items: DsAlert[] = [
    row({ id: "1", resolution_status: "unresolved" }),
    row({ id: "2", resolution_status: "in_progress" }),
    row({ id: "3", resolution_status: "resolved" }),
  ];

  it("status=open は未解消+対応中（解消以外）", () => {
    expect(filterDsAlertsByStatus(items, "open").map((a) => a.id)).toEqual(["1", "2"]);
  });

  it("status=resolved は解消のみ", () => {
    expect(filterDsAlertsByStatus(items, "resolved").map((a) => a.id)).toEqual(["3"]);
  });

  it("status=all は全件保持する", () => {
    expect(filterDsAlertsByStatus(items, "all")).toHaveLength(3);
  });

  it("type=disconnected & status=resolved で解消が無ければ0件", () => {
    const disconnectedItems: DsAlert[] = [
      row({ id: "1", alert_type: "disconnected", resolution_status: "unresolved" }),
    ];
    const filtered = filterDsAlertsByStatus(
      filterDsAlertsByType(disconnectedItems, "disconnected"),
      "resolved"
    );
    expect(filtered).toHaveLength(0);
  });
});

describe("sortDsAlerts", () => {
  it("detected_atの降順になる", () => {
    const items: DsAlert[] = [
      row({ id: "1", detected_at: "2026-08-26T03:00:00+09:00" }),
      row({ id: "2", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "3", detected_at: "2026-08-25T03:00:00+09:00" }),
    ];
    expect(sortDsAlerts(items).map((a) => a.id)).toEqual(["2", "1", "3"]);
  });

  it("detected_atが同値のときはidの昇順で安定させる（D-052と同じ規則）", () => {
    const items: DsAlert[] = [
      row({ id: "3", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "1", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "2", detected_at: "2026-08-27T03:00:00+09:00" }),
    ];
    expect(sortDsAlerts(items).map((a) => a.id)).toEqual(["1", "2", "3"]);
  });

  it("★同値のときの id は文字列の辞書順（\"10\" が \"9\" より先。数値順なら逆。指示書AX D11・表1 R12）★", () => {
    const items: DsAlert[] = [
      row({ id: "9", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "10", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "E-9", detected_at: "2026-08-27T03:00:00+09:00" }),
      row({ id: "E-10", detected_at: "2026-08-27T03:00:00+09:00" }),
    ];
    expect(sortDsAlerts(items).map((a) => a.id)).toEqual(["10", "9", "E-10", "E-9"]);
  });

  it("★入力の配列を書き換えない★", () => {
    const items: DsAlert[] = [row({ id: "2" }), row({ id: "1" })];
    sortDsAlerts(items);
    expect(items.map((a) => a.id)).toEqual(["2", "1"]);
  });
});

describe("summarizeDsAlerts（キーは unresolved / in_progress / resolved。指示書AX D10）", () => {
  it("unresolved/in_progress/resolvedをそれぞれ数える", () => {
    const items: DsAlert[] = [
      row({ id: "1", resolution_status: "unresolved" }),
      row({ id: "2", resolution_status: "unresolved" }),
      row({ id: "3", resolution_status: "in_progress" }),
      row({ id: "4", resolution_status: "resolved" }),
    ];
    expect(summarizeDsAlerts(items)).toEqual({ unresolved: 2, in_progress: 1, resolved: 1 });
  });

  it("0件でも壊れない", () => {
    expect(summarizeDsAlerts([])).toEqual({ unresolved: 0, in_progress: 0, resolved: 0 });
  });
});

describe("summaryはtype/statusフィルタから独立している（D-054と同じ規則）", () => {
  const items: DsAlert[] = [
    row({ id: "1", alert_type: "security_off", resolution_status: "unresolved" }),
    row({ id: "2", alert_type: "disconnected", resolution_status: "in_progress" }),
    row({ id: "3", alert_type: "disconnected", resolution_status: "resolved" }),
  ];

  it("typeやstatusを変えてもsummaryは同一（全アラートに対する集計）", () => {
    // route.ts と同じ組み立て順: summaryは全件（フィルタ前）に対して計算する
    const summaryBase = summarizeDsAlerts(items);
    const summaryAfterType = summarizeDsAlerts(items);
    const summaryAfterStatus = summarizeDsAlerts(items);

    expect(summaryBase).toEqual({ unresolved: 1, in_progress: 1, resolved: 1 });
    expect(summaryBase).toEqual(summaryAfterType);
    expect(summaryBase).toEqual(summaryAfterStatus);

    // 一覧側はtype/statusで変わる
    expect(filterDsAlertsByType(items, "security_off")).toHaveLength(1);
    expect(filterDsAlertsByStatus(items, "resolved")).toHaveLength(1);
  });
});
