import type { Bucket } from "@/lib/types";
import { MARGIN, isEmptyTimeseries, layoutTimeseries, niceTicks } from "./chartLayout";

function bucket(ts: number, queries: number, blocked = 0, cached = 0): Bucket {
	return { ts, queries, blocked, cached };
}

describe("niceTicks", () => {
	test("zero max yields a single zero tick", () => {
		expect(niceTicks(0)).toEqual([0]);
	});

	test("picks a 1/2/5 step and extends past max", () => {
		expect(niceTicks(7)).toEqual([0, 2, 4, 6, 8]);
		expect(niceTicks(100)).toEqual([0, 50, 100]);
		expect(niceTicks(1234)).toEqual([0, 500, 1000, 1500]);
	});
});

describe("isEmptyTimeseries", () => {
	test("true for no buckets and for all-zero buckets", () => {
		expect(isEmptyTimeseries([])).toBe(true);
		expect(isEmptyTimeseries([bucket(0, 0), bucket(60, 0)])).toBe(true);
	});

	test("false when any bucket has queries", () => {
		expect(isEmptyTimeseries([bucket(0, 0), bucket(60, 3)])).toBe(false);
	});
});

describe("layoutTimeseries", () => {
	test("segment heights are proportional and stack to the queries total", () => {
		const layout = layoutTimeseries([bucket(0, 100, 40, 10), bucket(60, 50, 0, 0)], 480, 240);
		const plotHeight = 240 - MARGIN.top - MARGIN.bottom;
		const baseline = MARGIN.top + plotHeight;
		const [first, second] = layout.bars;

		expect(layout.scaleMax).toBe(100);
		expect(first.other).toBe(50);
		expect(first.segments.blocked.height).toBeCloseTo(plotHeight * 0.4);
		expect(first.segments.cached.height).toBeCloseTo(plotHeight * 0.1);
		expect(first.segments.other.height).toBeCloseTo(plotHeight * 0.5);
		expect(first.segments.blocked.y + first.segments.blocked.height).toBeCloseTo(baseline);
		expect(first.segments.cached.y + first.segments.cached.height).toBeCloseTo(first.segments.blocked.y);
		expect(first.segments.other.y + first.segments.other.height).toBeCloseTo(first.segments.cached.y);
		expect(first.segments.other.y).toBeCloseTo(MARGIN.top);
		expect(second.segments.other.height).toBeCloseTo(plotHeight * 0.5);
	});

	test("clamps other at zero when blocked + cached exceed queries", () => {
		const layout = layoutTimeseries([bucket(0, 10, 8, 5)], 480, 240);
		expect(layout.bars[0].other).toBe(0);
		expect(layout.bars[0].segments.other.height).toBe(0);
	});

	test("zero data still lays out zero-height bars on a unit scale", () => {
		const layout = layoutTimeseries([bucket(0, 0), bucket(60, 0)], 480, 240);
		expect(layout.scaleMax).toBe(1);
		expect(layout.bars).toHaveLength(2);
		for (const bar of layout.bars) {
			expect(bar.segments.blocked.height).toBe(0);
			expect(bar.segments.cached.height).toBe(0);
			expect(bar.segments.other.height).toBe(0);
		}
		expect(layout.yTicks).toEqual([{ value: 0, y: MARGIN.top + (240 - MARGIN.top - MARGIN.bottom) }]);
	});

	test("single bucket fills the plot width minus the gap", () => {
		const layout = layoutTimeseries([bucket(0, 5, 1, 1)], 480, 240);
		const plotWidth = 480 - MARGIN.left - MARGIN.right;
		const bar = layout.bars[0];
		expect(bar.slot.width).toBeCloseTo(plotWidth);
		expect(bar.segments.blocked.width).toBeCloseTo(plotWidth - 2);
		expect(bar.segments.blocked.x).toBeCloseTo(MARGIN.left + 1);
		expect(layout.xTicks).toEqual([{ ts: 0, x: MARGIN.left + plotWidth / 2 }]);
	});

	test("x ticks thin out when buckets outnumber the label budget", () => {
		const buckets = Array.from({ length: 168 }, (_, i) => bucket(i * 3600, i));
		const layout = layoutTimeseries(buckets, 800, 240);
		expect(layout.xTicks.length).toBeLessThan(buckets.length / 10);
		expect(layout.xTicks[0].ts).toBe(0);
		const xs = layout.xTicks.map((tick) => tick.x);
		expect([...xs].sort((a, b) => a - b)).toEqual(xs);
	});

	test("empty bucket list yields no bars and no x ticks", () => {
		const layout = layoutTimeseries([], 480, 240);
		expect(layout.bars).toEqual([]);
		expect(layout.xTicks).toEqual([]);
		expect(layout.scaleMax).toBe(1);
	});
});
