import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import type { WeekendPlanData } from "@/types";

const {
  mockRequestJson,
  mockToastShow,
  mockFetch,
} = vi.hoisted(() => ({
  mockRequestJson: vi.fn(),
  mockToastShow: vi.fn(),
  mockFetch: vi.fn(),
}));

vi.mock("@/lib/fetcher", () => ({
  requestJson: mockRequestJson,
}));

vi.mock("@/hooks/useToast", () => ({
  useToast: () => ({ show: mockToastShow }),
}));

vi.stubGlobal("fetch", mockFetch);

import { useBlindboxPlan } from "./useBlindboxPlan";

const room = {
  id: "room-1",
  code: "ABC123",
  name: "周末契约",
  creatorId: "user-1",
  city: "上海",
  address: "黄浦区",
  lat: 31.23,
  lng: 121.47,
  poolCount: 2,
  members: [{ id: "user-1", username: "alice", avatar: "🐱" }],
};

const profile = { id: "user-1", username: "alice", avatar: "🐱" };

const generatedDays: WeekendPlanData[] = [
  {
    date: "周六",
    summary: "轻松逛吃",
    items: [
      {
        time: "10:00",
        activity: "看展",
        poi: "上海博物馆",
        address: "黄浦区人民大道201号",
        lat: 31.23,
        lng: 121.47,
        duration: 120,
        reason: "交通方便",
      },
    ],
  },
];

function createRequestJsonMock(opts: { acceptShouldFail?: boolean; latestPlan?: boolean } = {}) {
  const { acceptShouldFail = false, latestPlan = false } = opts;
  return async (url: string, init?: { method?: string; body?: unknown }) => {
    if (url === "/api/blindbox/plan?mode=pending") {
      return { pending: [] };
    }
    if (url === "/api/blindbox/plan?mode=latest&roomId=room-1") {
      return latestPlan
        ? { plan: { id: "plan-latest", days: generatedDays, endTime: null } }
        : { plan: null };
    }
    if (url === "/api/blindbox/plan" && init?.method === "POST") {
      return {
        id: "plan-1",
        days: generatedDays,
        createdAt: "2026-03-03T00:00:00.000Z",
      };
    }
    if (url === "/api/blindbox/plan" && init?.method === "PATCH") {
      const body = init.body as { action?: string };
      if (body?.action === "accept") {
        if (acceptShouldFail) throw new Error("接受契约失败");
        return { endTime: null };
      }
      return { ok: true };
    }
    throw new Error(`unexpected request: ${url}`);
  };
}

describe("useBlindboxPlan", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockFetch.mockRejectedValue(new Error("stream unavailable"));
    mockRequestJson.mockImplementation(createRequestJsonMock());
  });

  it("loads latest accepted plan into activeContract", async () => {
    mockRequestJson.mockImplementation(createRequestJsonMock({ latestPlan: true }));
    const setPhase = vi.fn();
    const fireConfetti = vi.fn();

    const { result } = renderHook(() =>
      useBlindboxPlan(room, profile, "pool", setPhase, fireConfetti),
    );

    await act(async () => {
      await result.current.fetchAcceptedPlan();
    });

    expect(mockRequestJson).toHaveBeenCalledWith("/api/blindbox/plan?mode=latest&roomId=room-1");
    expect(result.current.activeContract?.id).toBe("plan-latest");
    expect(result.current.activeContract?.days).toEqual(generatedDays);
  });

  it("falls back to normal plan API when stream generation fails", async () => {
    const setPhase = vi.fn();
    const fireConfetti = vi.fn();
    const { result } = renderHook(() =>
      useBlindboxPlan(room, profile, "pool", setPhase, fireConfetti),
    );

    await act(async () => {
      await result.current.handleGeneratePlan({
        date: "2026-03-07",
        startHour: 9,
        endHour: 18,
      });
    });

    expect(mockRequestJson).toHaveBeenCalledWith("/api/blindbox/plan", {
      method: "POST",
      body: {
        roomId: "room-1",
        availableTime: { date: "2026-03-07", startHour: 9, endHour: 18 },
      },
    });
    expect(setPhase).toHaveBeenCalledWith("planning");
    expect(setPhase).toHaveBeenCalledWith("plan_reveal");
    expect(result.current.planId).toBe("plan-1");
    expect(result.current.planDays).toEqual(generatedDays);
    expect(fireConfetti).toHaveBeenCalled();
  });

  it("accepts plan successfully after generation", async () => {
    const setPhase = vi.fn();
    const fireConfetti = vi.fn();
    const { result } = renderHook(() =>
      useBlindboxPlan(room, profile, "pool", setPhase, fireConfetti),
    );

    await act(async () => {
      await result.current.handleGeneratePlan({
        date: "2026-03-08",
        startHour: 10,
        endHour: 19,
      });
    });
    fireConfetti.mockClear();
    mockToastShow.mockClear();

    await act(async () => {
      await result.current.handleAcceptPlan();
    });

    expect(mockRequestJson).toHaveBeenCalledWith("/api/blindbox/plan", {
      method: "PATCH",
      body: { planId: "plan-1", action: "accept" },
    });
    expect(result.current.planAccepted).toBe(true);
    expect(result.current.activeContract?.id).toBe("plan-1");
    expect(fireConfetti).toHaveBeenCalledTimes(1);
    expect(mockToastShow).toHaveBeenCalledWith("契约已接受！");
  });

  it("does not mark accepted when accept request fails", async () => {
    mockRequestJson.mockImplementation(createRequestJsonMock({ acceptShouldFail: true }));
    const setPhase = vi.fn();
    const fireConfetti = vi.fn();
    const { result } = renderHook(() =>
      useBlindboxPlan(room, profile, "pool", setPhase, fireConfetti),
    );

    await act(async () => {
      await result.current.handleGeneratePlan({
        date: "2026-03-09",
        startHour: 10,
        endHour: 20,
      });
    });
    fireConfetti.mockClear();
    mockToastShow.mockClear();

    await act(async () => {
      await result.current.handleAcceptPlan();
    });

    await waitFor(() => {
      expect(result.current.planAccepted).toBe(false);
    });
    expect(fireConfetti).not.toHaveBeenCalled();
    expect(mockToastShow).toHaveBeenCalledWith("接受契约失败");
  });
});
