// ステップメール配信タイミング拡張（schema_v3, 2026-07-15）のテスト
import { describe, expect, it } from "vitest";
import { computeStepSendAt, enrollSubscriber, runScheduler } from "../../src/core.js";
import { DryRunMailer } from "../../src/mailer.js";
import { InMemoryStore } from "../../src/store.js";
import { authed, login, makeClock, makeConfig, setup } from "../helpers.js";

const MIN = 60_000;
const HOUR = 3_600_000;
const DAY = 86_400_000;
const JST = 9 * HOUR;

/** JSTの日時 → epoch ms */
function jst(y: number, mo: number, d: number, h: number, mi: number): number {
  return Date.UTC(y, mo - 1, d, h, mi) - JST;
}

describe("computeStepSendAt", () => {
  const base = jst(2026, 7, 15, 12, 0); // JST 7/15 12:00

  it("immediate は基準時刻そのまま", () => {
    expect(
      computeStepSendAt({ timing_type: "immediate", delay_minutes: 999, delay_days: 9, send_time: "23:00" }, base),
    ).toBe(base);
  });

  it("relative は delay_minutes 後", () => {
    expect(
      computeStepSendAt({ timing_type: "relative", delay_minutes: 90, delay_days: 0, send_time: null }, base),
    ).toBe(base + 90 * MIN);
  });

  it("day_time: 1日後の 09:00 JST", () => {
    expect(
      computeStepSendAt({ timing_type: "day_time", delay_minutes: 0, delay_days: 1, send_time: "09:00" }, base),
    ).toBe(jst(2026, 7, 16, 9, 0));
  });

  it("day_time: 0日後で時刻が未来なら当日", () => {
    expect(
      computeStepSendAt({ timing_type: "day_time", delay_minutes: 0, delay_days: 0, send_time: "18:30" }, base),
    ).toBe(jst(2026, 7, 15, 18, 30));
  });

  it("day_time: 0日後で時刻が過ぎていたら翌日に繰り越し（即時配信しない）", () => {
    expect(
      computeStepSendAt({ timing_type: "day_time", delay_minutes: 0, delay_days: 0, send_time: "09:00" }, base),
    ).toBe(jst(2026, 7, 16, 9, 0));
  });

  it("day_time: send_time 不正なら relative にフォールバック", () => {
    expect(
      computeStepSendAt({ timing_type: "day_time", delay_minutes: 15, delay_days: 0, send_time: "bad" }, base),
    ).toBe(base + 15 * MIN);
  });

  it("後方互換: timing_type 未定義（既存行）は relative 扱い", () => {
    const legacy = { delay_minutes: 60 } as unknown as Parameters<typeof computeStepSendAt>[0];
    expect(computeStepSendAt(legacy, base)).toBe(base + HOUR);
  });
});

describe("day_time ステップのスケジューラ連携", () => {
  it("登録→翌日9時に1通目、その翌日9時に2通目", async () => {
    const clock = makeClock(jst(2026, 7, 15, 12, 0));
    const store = new InMemoryStore(clock.now);
    const mailer = new DryRunMailer();
    const cfg = makeConfig({ dryRun: true });
    const list = await store.createList("L");
    const camp = await store.createCampaign({ name: "C", list_id: list.id });
    for (const i of [0, 1])
      await store.addStepEmail({
        campaign_id: camp.id,
        step_order: i,
        delay_minutes: 0,
        subject: `S${i + 1}`,
        body_html: "<p>x</p>",
        timing_type: "day_time",
        delay_days: 1,
        send_time: "09:00",
      });
    const { subscriber } = await store.upsertSubscriber("a@x.com", null, "t");
    await enrollSubscriber(store, camp.id, subscriber.id, clock.now());
    let [e] = await store.listEnrollments(camp.id);
    expect(e.next_send_at).toBe(jst(2026, 7, 16, 9, 0));

    // 翌日9時: 1通目処理 → 次は7/17 9:00
    clock.set(jst(2026, 7, 16, 9, 0));
    await runScheduler(store, mailer, cfg, clock.now());
    [e] = await store.listEnrollments(camp.id);
    expect(e.current_step).toBe(1);
    expect(e.next_send_at).toBe(jst(2026, 7, 17, 9, 0));
  });
});

describe("ステップ CRUD / 並べ替え（Store）", () => {
  async function seed() {
    const store = new InMemoryStore(makeClock().now);
    const list = await store.createList("L");
    const camp = await store.createCampaign({ name: "C", list_id: list.id });
    const s1 = await store.addStepEmail({ campaign_id: camp.id, step_order: 0, delay_minutes: 0, subject: "A", body_html: "a" });
    const s2 = await store.addStepEmail({ campaign_id: camp.id, step_order: 1, delay_minutes: 60, subject: "B", body_html: "b" });
    const s3 = await store.addStepEmail({ campaign_id: camp.id, step_order: 2, delay_minutes: 60, subject: "C", body_html: "c" });
    return { store, camp, s1, s2, s3 };
  }

  it("updateStepEmail で件名とタイミングを変更できる", async () => {
    const { store, s1 } = await seed();
    await store.updateStepEmail(s1.id, { subject: "A2", timing_type: "day_time", delay_days: 3, send_time: "20:00", delay_minutes: 0 });
    const after = await store.getStepEmail(s1.id);
    expect(after?.subject).toBe("A2");
    expect(after?.timing_type).toBe("day_time");
    expect(after?.delay_days).toBe(3);
    expect(after?.send_time).toBe("20:00");
  });

  it("deleteStepEmail で削除できる", async () => {
    const { store, camp, s2 } = await seed();
    await store.deleteStepEmail(s2.id);
    const steps = await store.listStepEmails(camp.id);
    expect(steps.map((s) => s.subject)).toEqual(["A", "C"]);
  });

  it("reorderStepEmails で並び順を入れ替えられる", async () => {
    const { store, camp, s1, s2, s3 } = await seed();
    await store.reorderStepEmails(camp.id, [s3.id, s1.id, s2.id]);
    const steps = await store.listStepEmails(camp.id);
    expect(steps.map((s) => s.subject)).toEqual(["C", "A", "B"]);
    expect(steps.map((s) => s.step_order)).toEqual([0, 1, 2]);
  });

  it("updateCampaign で一時停止できる", async () => {
    const { store, camp } = await seed();
    await store.updateCampaign(camp.id, { status: "paused" });
    expect((await store.getCampaign(camp.id))?.status).toBe("paused");
  });
});

describe("ステップ管理 API", () => {
  async function seedApi() {
    const s = setup();
    const cookie = await login(s.app);
    const list = await (await s.app.request("/api/lists", authed(cookie, { name: "L" }))).json();
    const campRes = await (
      await s.app.request("/api/campaigns", authed(cookie, { name: "C", list_id: list.list?.id ?? 1 }))
    ).json();
    return { ...s, cookie, cid: campRes.campaign.id as number };
  }

  it("step_order 省略で末尾に自動採番", async () => {
    const { app, cookie, cid } = await seedApi();
    const r1 = await (
      await app.request(`/api/campaigns/${cid}/steps`, authed(cookie, { subject: "S1", body_html: "b", timing_type: "immediate" }))
    ).json();
    const r2 = await (
      await app.request(`/api/campaigns/${cid}/steps`, authed(cookie, { subject: "S2", body_html: "b", timing_type: "relative", delay_minutes: 60 }))
    ).json();
    expect(r1.step.step_order).toBe(0);
    expect(r2.step.step_order).toBe(1);
  });

  it("day_time は send_time 必須", async () => {
    const { app, cookie, cid } = await seedApi();
    const res = await app.request(
      `/api/campaigns/${cid}/steps`,
      authed(cookie, { subject: "S", body_html: "b", timing_type: "day_time" }),
    );
    expect(res.status).toBe(400);
  });

  it("PUT で編集、DELETE で削除、reorder で並べ替え", async () => {
    const { app, cookie, cid } = await seedApi();
    const mk = async (subject: string) =>
      (
        await (
          await app.request(`/api/campaigns/${cid}/steps`, authed(cookie, { subject, body_html: "b", timing_type: "immediate" }))
        ).json()
      ).step;
    const a = await mk("A");
    const b = await mk("B");

    const put = await app.request(`/api/campaigns/${cid}/steps/${a.id}`, {
      ...authed(cookie, { subject: "A2" }),
      method: "PUT",
    });
    expect((await put.json()).step.subject).toBe("A2");

    const reorder = await app.request(
      `/api/campaigns/${cid}/steps/reorder`,
      authed(cookie, { ids: [b.id, a.id] }),
    );
    expect((await reorder.json()).steps.map((s: { subject: string }) => s.subject)).toEqual(["B", "A2"]);

    // 不正な並べ替え（IDの過不足）は拒否
    const bad = await app.request(`/api/campaigns/${cid}/steps/reorder`, authed(cookie, { ids: [b.id] }));
    expect(bad.status).toBe(400);

    const del = await app.request(`/api/campaigns/${cid}/steps/${b.id}`, {
      method: "DELETE",
      headers: { Cookie: cookie },
    });
    expect((await del.json()).ok).toBe(true);
  });

  it("テスト送信: DRY_RUN中は dryRun=true が返る", async () => {
    const { app, cookie, cid, mailer } = await seedApi();
    const s = (
      await (
        await app.request(`/api/campaigns/${cid}/steps`, authed(cookie, { subject: "S", body_html: "b", timing_type: "immediate" }))
      ).json()
    ).step;
    const res = await (
      await app.request(`/api/campaigns/${cid}/steps/${s.id}/test`, authed(cookie, { email: "boss@example.com" }))
    ).json();
    expect(res.ok).toBe(true);
    expect(res.dryRun).toBe(true);
    // DryRunMailer には記録される（実配信はされない設計）
    expect((mailer as DryRunMailer).sent.length).toBe(1);
    expect((mailer as DryRunMailer).sent[0].msg.subject).toContain("[テスト]");
  });

  it("campaign PUT: 一時停止と名称変更", async () => {
    const { app, cookie, cid } = await seedApi();
    const res = await app.request(`/api/campaigns/${cid}`, {
      ...authed(cookie, { name: "新名称", status: "paused" }),
      method: "PUT",
    });
    const j = await res.json();
    expect(j.campaign.name).toBe("新名称");
    expect(j.campaign.status).toBe("paused");
  });
});
