// ObserveChartCard: lazy fetch lifecycle (loading → error + retry → empty →
// chart) against a mocked api-client, the pure observeChartData pivot, and
// one <Line> per response line with colorKey-derived strokes. jsdom never
// reports a container size, so the real ResponsiveContainer would defer
// rendering forever — it is mocked to clone the chart to an explicit
// 600×200, which mounts the lines' SVG.

import { afterEach, describe, expect, it, vi } from "vitest";
import type { ReactElement } from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { ObserveChartCard, observeChartData } from "@/components/regions/ObserveChartCard";
import { fetchObserveSeries } from "@/lib/api-client";
import { chartSeriesColor } from "@/lib/chart";
import {
  findObserveChart,
  findObserveDashboard,
} from "@/engine/observe-dashboards";
import type { TrendWindowKey } from "@/engine/trend-window";
import type { ObserveSeriesResponse } from "@/lib/api-types";

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

vi.mock("recharts", async (importOriginal) => {
  const actual = await importOriginal<typeof import("recharts")>();
  const { cloneElement } = await import("react");
  return {
    ...actual,
    ResponsiveContainer: ({ children }: { children: ReactElement }) =>
      cloneElement(children as ReactElement<{ width?: number; height?: number }>, {
        width: 600,
        height: 200,
      }),
  };
});

const mockedFetch = vi.mocked(fetchObserveSeries);

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

const props = {
  regionKey: "aws-us-west-2",
  dashboardId: "vmcluster",
  chart: findObserveChart(findObserveDashboard("vmcluster")!, "cpu-max")!,
  windowKey: "24h" as TrendWindowKey,
};

function seedSeries(): ObserveSeriesResponse {
  const t0 = Date.UTC(2026, 8, 5, 0, 0) / 1000;
  return {
    dashboard: "vmcluster",
    chart: "cpu-max",
    window: "24h",
    series: [
      {
        legend: "max · vmstorage-vmcluster",
        colorKey: "vmstorage-vmcluster",
        values: [
          { t: t0, v: 0.5 },
          { t: t0 + 600, v: 0.6 },
          { t: t0 + 1200, v: 0.55 },
        ],
      },
      {
        legend: "min · vmstorage-vmcluster",
        colorKey: "vmstorage-vmcluster",
        values: [
          { t: t0, v: 0.2 },
          { t: t0 + 600, v: 0.25 },
          { t: t0 + 1200, v: 0.22 },
        ],
      },
      {
        legend: "max · vmselect-vmcluster",
        colorKey: "vmselect-vmcluster",
        values: [
          { t: t0, v: 0.3 },
          { t: t0 + 600, v: 0.35 },
          { t: t0 + 1200, v: 0.31 },
        ],
      },
    ],
  };
}

describe("observeChartData", () => {
  it("unions timestamps across lines (sorted by t), leaving gaps absent", () => {
    const t0 = Date.UTC(2026, 8, 5, 0, 0) / 1000;
    const s: ObserveSeriesResponse = {
      dashboard: "vmcluster",
      chart: "cpu-max",
      window: "24h",
      series: [
        { legend: "max · a", colorKey: "a", values: [{ t: t0, v: 1 }, { t: t0 + 3600, v: 2 }] },
        { legend: "min · a", colorKey: "a", values: [{ t: t0 + 3600, v: 0.5 }] },
      ],
    };
    expect(observeChartData(s)).toEqual([
      { t: t0, "max · a": 1 },
      { t: t0 + 3600, "min · a": 0.5, "max · a": 2 },
    ]);
  });

  it("returns no rows when every line is empty", () => {
    const s: ObserveSeriesResponse = {
      dashboard: "vmcluster",
      chart: "cpu-max",
      window: "24h",
      series: [{ legend: "x", colorKey: "x", values: [] }],
    };
    expect(observeChartData(s)).toEqual([]);
  });
});

describe("ObserveChartCard", () => {
  it("fetches on mount at the passed window and shows the loading state", async () => {
    mockedFetch.mockReturnValue(new Promise(() => {}) as never);
    render(<ObserveChartCard {...props} />);
    expect(mockedFetch).toHaveBeenCalledWith(props.regionKey, "vmcluster", "cpu-max", "24h");
    expect(screen.getByText("loading chart…")).toBeTruthy();
  });

  it("refetches when the windowKey prop changes", async () => {
    mockedFetch.mockReturnValue(new Promise(() => {}) as never);
    const { rerender } = render(<ObserveChartCard {...props} />);
    rerender(<ObserveChartCard {...props} windowKey="7d" />);
    expect(mockedFetch).toHaveBeenLastCalledWith(props.regionKey, "vmcluster", "cpu-max", "7d");
  });

  it("shows an inline error with retry, and retry refetches", async () => {
    mockedFetch
      .mockRejectedValueOnce(new Error("dataplane down") as never)
      .mockResolvedValueOnce(seedSeries() as never);
    render(<ObserveChartCard {...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({
      dashboard: "vmcluster",
      chart: "cpu-max",
      window: "24h",
      series: [{ legend: "x", colorKey: "x", values: [{ t: 1, v: 1 }] }],
    } as ObserveSeriesResponse);
    render(<ObserveChartCard {...props} />);
    await waitFor(() => {
      expect(screen.getByText("Need at least two samples for a trend.")).toBeTruthy();
    });
  });

  it("renders one line per response line, stroke by colorKey", async () => {
    mockedFetch.mockResolvedValue(seedSeries() as never);
    const { container } = render(<ObserveChartCard {...props} />);
    await waitFor(() => {
      expect(container.querySelectorAll("path.recharts-curve").length).toBe(3);
    });
    // Color identity is the colorKey palette hash — the two vmstorage lines
    // share one stroke, vmselect gets the other.
    const strokes = new Set(
      [...container.querySelectorAll("path.recharts-curve")].map((p) => p.getAttribute("stroke")),
    );
    expect(strokes).toEqual(
      new Set([chartSeriesColor("vmstorage-vmcluster"), chartSeriesColor("vmselect-vmcluster")]),
    );
  });

  it("toggles a line off and back on via its legend entry", async () => {
    mockedFetch.mockResolvedValue(seedSeries() as never);
    const { container } = render(<ObserveChartCard {...props} />);
    await waitFor(() => {
      expect(container.querySelectorAll("path.recharts-curve").length).toBe(3);
    });
    const legendText = screen.getByText("min · vmstorage-vmcluster");
    fireEvent.click(legendText.closest(".recharts-legend-item") ?? legendText);
    await waitFor(() => {
      expect(container.querySelectorAll("path.recharts-curve").length).toBe(2);
    });
    fireEvent.click(screen.getByText("min · vmstorage-vmcluster").closest(".recharts-legend-item")!);
    await waitFor(() => {
      expect(container.querySelectorAll("path.recharts-curve").length).toBe(3);
    });
  });
});
