// NodeUsageChart: lazy fetch lifecycle (loading → error + retry → empty →
// chart) against a mocked api-client, plus the pure nodeUsageChartData
// mapper (usage ÷ allocatable). No recharts layout assertions — jsdom
// renders zero-size containers (WorkloadTrendChart's chart is untested for
// the same reason).

import { afterEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { NodeUsageChart, nodeUsageChartData } from "@/components/regions/NodeUsageChart";
import { fetchNodeUsage } from "@/lib/api-client";
import type { NodeUsagePoint, NodeUsageSeriesResponse } from "@/lib/api-types";

vi.mock("@/lib/api-client", () => ({
  fetchNodeUsage: vi.fn(),
}));

const mockedFetch = vi.mocked(fetchNodeUsage);

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

const props = {
  regionKey: "aws-us-west-2",
  node: "ip-10-0-0-1.ec2.internal",
  allocatableCpu: 3.92,
  allocatableMemGib: 14.28,
};

function point(t: number, cpuCores: number, memGiB: number): NodeUsagePoint {
  return { t, cpuCores, memGiB };
}

describe("nodeUsageChartData", () => {
  it("maps engine points to fractions of allocatable with UTC+8 tick labels", () => {
    const t0 = Date.UTC(2026, 8, 5, 0, 0) / 1000; // 2026-09-05 00:00 UTC -> 08:00 Beijing
    const rows = nodeUsageChartData([point(t0, 1.96, 7.14), point(t0 + 3600, 0.98, 3.57)], 3.92, 14.28);
    expect(rows).toEqual([
      { tick: "09-05 08:00", cpu: 0.5, mem: 0.5 },
      { tick: "09-05 09:00", cpu: 0.25, mem: 0.25 },
    ]);
  });

  it("falls back to 0 when allocatable is missing (avoiding NaN rows)", () => {
    const rows = nodeUsageChartData([point(100, 1, 2)], 0, 0);
    expect(rows).toEqual([{ tick: expect.any(String), cpu: 0, mem: 0 }]);
  });
});

describe("NodeUsageChart", () => {
  it("fetches on mount at the default window and shows the loading state", async () => {
    mockedFetch.mockReturnValue(new Promise(() => {}) as never);
    render(<NodeUsageChart {...props} />);
    expect(mockedFetch).toHaveBeenCalledWith(props.regionKey, props.node, "24h");
    expect(screen.getByText("loading node usage…")).toBeTruthy();
  });

  it("refetches when the window preset changes", async () => {
    mockedFetch.mockReturnValue(new Promise(() => {}) as never);
    render(<NodeUsageChart {...props} />);
    fireEvent.change(screen.getByRole("combobox", { name: "trend time range" }), {
      target: { value: "7d" },
    });
    expect(mockedFetch).toHaveBeenCalledWith(props.regionKey, props.node, "7d");
    expect(screen.getByText("loading node usage…")).toBeTruthy();
  });

  it("shows an inline error with retry, and retry refetches", async () => {
    mockedFetch
      .mockRejectedValueOnce(new Error("dataplane down") as never)
      .mockResolvedValueOnce({ points: [point(1, 1, 2), point(2, 1, 2)] } as never);
    render(<NodeUsageChart {...props} />);
    await waitFor(() => {
      expect(screen.getByText(/dataplane down/)).toBeTruthy();
    });
    fireEvent.click(screen.getByRole("button", { name: "retry" }));
    expect(mockedFetch).toHaveBeenCalledTimes(2);
    await waitFor(() => {
      expect(screen.queryByText(/dataplane down/)).toBeNull();
    });
  });

  it("renders the empty state for fewer than two samples", async () => {
    mockedFetch.mockResolvedValue({ points: [point(1, 1, 2)] } as NodeUsageSeriesResponse);
    render(<NodeUsageChart {...props} />);
    await waitFor(() => {
      expect(screen.getByText("Need at least two samples for a trend.")).toBeTruthy();
    });
  });

  it("renders the no-allocatable note when the row lacks allocatable", async () => {
    mockedFetch.mockReturnValue(new Promise(() => {}) as never);
    render(<NodeUsageChart {...props} allocatableCpu={0} />);
    expect(screen.getByText("No allocatable recorded for this node.")).toBeTruthy();
  });

  it("renders the chart for a full series without erroring", async () => {
    const series: NodeUsageSeriesResponse = {
      points: [
        point(100, 1, 4),
        point(200, 2, 8),
        point(300, 1.5, 6),
      ],
    };
    mockedFetch.mockResolvedValue(series);
    const { container } = render(<NodeUsageChart {...props} />);
    // jsdom gives ResponsiveContainer zero size, so the inner .recharts-wrapper
    // never mounts — the responsive container itself is the chart witness.
    await waitFor(() => {
      expect(container.querySelector(".recharts-responsive-container")).toBeTruthy();
    });
    expect(screen.queryByText(/unavailable/i)).toBeNull();
  });
});
