/**
 * WorkspaceClient 測試（塊 2-review M1：overlay 未就緒不繪製）
 *
 * 聚焦 M1：CameraFeed `<img>` 是 height:auto（實際高依 MJPEG 比例），overlay 必須等
 *   ResizeObserver 回報**真實顯示尺寸**後才繪製；在那之前不可用寫死猜測換算 → 否則 bbox 錯位。
 *
 * 策略：
 *   - 用真實 device-store（setState 灌一台 online 裝置）+ 真實 inference-store
 *   - mock `@/lib/api`（start 回 streamUrl）、mock `@/hooks/use-inference-stream`（no-op）
 *   - 用可控 ResizeObserver：預設「不」觸發 callback（模擬尺寸尚未回報）；
 *     另一測試手動觸發 callback 模擬尺寸就緒
 */
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { LocaleProvider } from "@/lib/i18n/context";
import { useDeviceStore } from "@/stores/device-store";
import { useInferenceStore } from "@/stores/inference-store";

// mock api：start 回 streamUrl；stop 直接 resolve
const post = vi.fn();
vi.mock("@/lib/api", async () => {
  const actual = await vi.importActual<typeof import("@/lib/api")>("@/lib/api");
  return { ...actual, api: { ...actual.api, post: (...a: unknown[]) => post(...a) } };
});

// mock WS hook（避免真連線）
vi.mock("@/hooks/use-inference-stream", () => ({
  useInferenceStream: vi.fn(),
}));

// 可控 ResizeObserver：把 callback 存起來，測試決定何時觸發
let roCallbacks: ResizeObserverCallback[] = [];
class ControllableRO {
  cb: ResizeObserverCallback;
  constructor(cb: ResizeObserverCallback) {
    this.cb = cb;
    roCallbacks.push(cb);
  }
  observe() {}
  unobserve() {}
  disconnect() {}
}

import { useInferenceStream } from "@/hooks/use-inference-stream";

import { WorkspaceClient } from "./workspace-client";

/** 取 useInferenceStream 最後一次呼叫的 enabled 參數（第 2 個引數）。 */
function lastCameraWsEnabled(): boolean | undefined {
  const mock = vi.mocked(useInferenceStream);
  const call = mock.mock.calls.at(-1);
  return call?.[1] as boolean | undefined;
}

function seedOnlineDevice() {
  useDeviceStore.setState({
    selectedDevice: {
      id: "dev-1",
      name: "KL520",
      remoteStatus: "online",
      lastSeenAt: null,
      // WP-C（ADR-018）：camera/media/WS 操作識別值 = serialNumber
      serialNumber: "KN00123456",
    } as ReturnType<typeof useDeviceStore.getState>["selectedDevice"],
    isLoading: false,
  });
}

/** WP-C：seed 一台線上但「未回報序號」的裝置（舊資料 / 假序號 → null）。 */
function seedOnlineDeviceWithoutSerial() {
  useDeviceStore.setState({
    selectedDevice: {
      id: "dev-1",
      name: "KL520",
      remoteStatus: "online",
      lastSeenAt: null,
      serialNumber: null,
    } as ReturnType<typeof useDeviceStore.getState>["selectedDevice"],
    isLoading: false,
  });
}

function renderClient() {
  return render(
    <LocaleProvider>
      <WorkspaceClient deviceId="dev-1" />
    </LocaleProvider>,
  );
}

beforeEach(() => {
  post.mockReset();
  roCallbacks = [];
  (globalThis as { ResizeObserver?: unknown }).ResizeObserver = ControllableRO;
  useInferenceStore.setState({
    result: null,
    results: [],
    fps: 0,
    avgLatency: 0,
    batchResults: {},
    confidenceThreshold: 0.5,
  });
  seedOnlineDevice();
});

afterEach(() => {
  vi.clearAllMocks();
});

describe("WorkspaceClient — overlay 尺寸就緒閘門（M1）", () => {
  it("start 後、尺寸尚未回報前不繪製 overlay", async () => {
    post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
    // 先放一筆偵測結果（模擬第一筆結果早於 ResizeObserver 回報）
    useInferenceStore.setState({
      result: {
        taskType: "detection",
        timestamp: Date.now(),
        latencyMs: 10,
        detections: [
          { label: "x", confidence: 0.9, bbox: { x: 0, y: 0, width: 0.5, height: 0.5 } },
        ],
      },
    });

    renderClient();
    fireEvent.click(screen.getByText("開始推論"));

    // CameraFeed 應已顯示（有 img），但 overlay 尚未繪製（feedSize 為 null）
    await waitFor(() => {
      expect(screen.getByTestId("camera-feed-img")).toBeInTheDocument();
    });
    expect(screen.queryByTestId("camera-overlay")).not.toBeInTheDocument();
  });

  it("ResizeObserver 回報真實尺寸後才繪製 overlay", async () => {
    post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
    renderClient();
    fireEvent.click(screen.getByText("開始推論"));

    await waitFor(() => {
      expect(screen.getByTestId("camera-feed-img")).toBeInTheDocument();
    });
    // 尚未回報 → 無 overlay
    expect(screen.queryByTestId("camera-overlay")).not.toBeInTheDocument();

    // 模擬 ResizeObserver 回報真實顯示尺寸（非 4:3，例如 640×360）
    act(() => {
      const img = screen.getByTestId("camera-feed-img");
      roCallbacks.forEach((cb) =>
        cb(
          [
            { target: img, contentRect: { width: 640, height: 360 } } as unknown as ResizeObserverEntry,
          ],
          {} as ResizeObserver,
        ),
      );
    });

    await waitFor(() => {
      expect(screen.getByTestId("camera-overlay")).toBeInTheDocument();
    });
    // canvas 尺寸用回報的真實值（360，非寫死 480）
    const canvas = screen.getByTestId("camera-overlay") as HTMLCanvasElement;
    expect(canvas.getAttribute("height")).toBe("360");
  });
});

/**
 * 切換 Radix Tab。Radix Tabs 預設 activationMode="automatic"（focus 即切換），
 * jsdom 下單純 click 不一定觸發 onValueChange，故同時 fire focus + click 確保切換。
 */
async function switchTab(name: string) {
  const trigger = screen.getByRole("tab", { name });
  await act(async () => {
    fireEvent.focus(trigger);
    fireEvent.click(trigger);
  });
}

describe("WorkspaceClient — 切 tab 時 camera 串流 / WS 正確關閉（S-1，塊3 最高風險路徑）", () => {
  it("推論中切離 camera tab → 呼叫 /api/camera/stop + camera WS enabled 變 false", async () => {
    post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
    renderClient();

    // 開始 camera 推論
    fireEvent.click(screen.getByText("開始推論"));
    await waitFor(() => {
      expect(screen.getByTestId("camera-feed-img")).toBeInTheDocument();
    });
    // camera WS 此時應為 enabled=true（camera tab + 推論中 + 線上）
    expect(lastCameraWsEnabled()).toBe(true);
    // 尚未呼叫 stop
    expect(post).not.toHaveBeenCalledWith("/api/camera/stop", expect.anything());

    // 切到 image tab
    await switchTab("圖片");

    // handleTabChange 應自動停掉 camera（POST /api/camera/stop）
    // WP-C（ADR-018）：body 的 deviceId 欄位帶 serialNumber（非雲端 UUID）
    await waitFor(() => {
      expect(post).toHaveBeenCalledWith("/api/camera/stop", {
        deviceId: "KN00123456",
      });
    });
    // camera 串流已清（isRunning=false）→ camera 的 useInferenceStream enabled 變 false（不殘留連線）
    await waitFor(() => {
      expect(lastCameraWsEnabled()).toBe(false);
    });
    // camera feed 已不在畫面（切到 image tab，且串流已清）
    expect(screen.queryByTestId("camera-feed-img")).not.toBeInTheDocument();
  });

  it("未推論時切 tab → 不呼叫 stop（無殘留可清）", async () => {
    renderClient();
    // 沒按開始，直接切到 video tab
    await switchTab("影片");
    expect(post).not.toHaveBeenCalledWith("/api/camera/stop", expect.anything());
    // camera WS 一直是 disabled
    expect(lastCameraWsEnabled()).toBe(false);
  });

  it("切到 image tab 後 camera WS 保持 false（媒體 tab 有自己的 WS，兩者互斥）", async () => {
    post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
    renderClient();
    fireEvent.click(screen.getByText("開始推論"));
    await waitFor(() => expect(screen.getByTestId("camera-feed-img")).toBeInTheDocument());

    await switchTab("圖片");
    await waitFor(() => expect(lastCameraWsEnabled()).toBe(false));

    // 停留在 image tab，camera WS 不應又被打開
    expect(lastCameraWsEnabled()).toBe(false);
  });
});

describe("WorkspaceClient — serial 路由（WP-C / ADR-018）", () => {
  it("camera start 的 body deviceId 帶 serialNumber、camera WS 識別值也是 serialNumber", async () => {
    post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
    renderClient();

    fireEvent.click(screen.getByText("開始推論"));
    await waitFor(() => {
      expect(post).toHaveBeenCalledWith("/api/camera/start", {
        deviceId: "KN00123456",
      });
    });

    // camera WS 的第一個參數（識別值）= serial，且 enabled=true
    const wsCall = vi.mocked(useInferenceStream).mock.calls.at(-1);
    expect(wsCall?.[0]).toBe("KN00123456");
    expect(wsCall?.[1]).toBe(true);
  });

  it("serial 為空：開始推論 disabled + 顯示 no-serial banner、不打 camera start", () => {
    seedOnlineDeviceWithoutSerial();
    renderClient();

    const startBtn = screen.getByText("開始推論").closest("button");
    expect(startBtn).toBeDisabled();
    expect(screen.getByTestId("workspace-no-serial-banner")).toBeInTheDocument();

    fireEvent.click(screen.getByText("開始推論"));
    expect(post).not.toHaveBeenCalled();
  });

  it("serial 為空：media tab 顯示 no-serial 提示、不出現上傳區", async () => {
    seedOnlineDeviceWithoutSerial();
    renderClient();

    await switchTab("圖片");
    expect(screen.getByTestId("media-no-serial")).toBeInTheDocument();
    expect(screen.queryByTestId("media-uploader-image")).not.toBeInTheDocument();
  });

  it("serial 有值：media tab 正常顯示上傳區", async () => {
    renderClient();
    await switchTab("圖片");
    expect(screen.getByTestId("media-uploader-image")).toBeInTheDocument();
    expect(screen.queryByTestId("media-no-serial")).not.toBeInTheDocument();
  });
});
