import { render } from "@testing-library/react";
import {
	CHART_HEIGHT,
	ChartFrame,
	bandPaddingInner,
	bandScale,
	labelTickValues,
	plotArea,
	useMeasuredWidth,
	valueScale,
	valueTicks,
} from "./chartKit";

describe("useMeasuredWidth", () => {
	/**
	 * jsdom lays nothing out, so `clientWidth` is 0 there and a chart scaled to it
	 * would draw a zero-width plot. Every chart test depends on this fallback.
	 */
	test("an unmeasurable container falls back to 640", () => {
		function Probe() {
			const [ref, width] = useMeasuredWidth();
			return <div ref={ref} data-testid="probe" data-width={width} />;
		}
		const { getByTestId } = render(<Probe />);
		expect(getByTestId("probe").getAttribute("data-width")).toBe("640");
	});
});

describe("valueScale", () => {
	test("the data maximum is nicened outward and the ticks land on round numbers", () => {
		const scale = valueScale(1780, [240, 0]);
		expect(scale.domain()).toEqual([0, 1800]);
		expect(scale.ticks(5)).toEqual([0, 500, 1000, 1500]);
		expect(valueTicks(scale)).toEqual([0, 500, 1000, 1500]);
	});
});

describe("bandPaddingInner", () => {
	/**
	 * The gap is a constant number of pixels, so the fraction it takes of one slot
	 * has to fall as the slots get wider. 24 hourly columns in a 1388px plot is
	 * the 24h window on a full-width panel.
	 */
	test("the fraction is two pixels per column of the plot's width", () => {
		expect(bandPaddingInner(24, 1388)).toBeCloseTo((2 * 24) / 1388, 12);
		expect(bandPaddingInner(24, 1388)).toBeCloseTo(0.034582, 6);
	});

	/**
	 * Past the cap the gap would be most of the slot and the columns would vanish
	 * into slivers, so it stops at half the slot and the bars stay visible.
	 */
	test("the gap never takes more than half a slot", () => {
		expect(bandPaddingInner(1000, 100)).toBe(0.5);
	});

	test("no columns and no width mean no gap to compute", () => {
		expect(bandPaddingInner(0, 640)).toBe(0);
		expect(bandPaddingInner(24, 0)).toBe(0);
	});
});

describe("labelTickValues", () => {
	/** A week of hourly buckets in a panel the width of a laptop window. */
	test("labels thin out to one every 21 buckets at 748px of plot", () => {
		const timestamps = Array.from({ length: 168 }, (_, i) => i * 3600);
		expect(labelTickValues(timestamps, 748)).toEqual([0, 75600, 151200, 226800, 302400, 378000, 453600, 529200]);
	});

	test("every bucket is labelled when they all fit", () => {
		expect(labelTickValues([0, 3600, 7200], 748)).toEqual([0, 3600, 7200]);
	});
});

/** The frame on its own, at the width the charts draw at in jsdom. */
function renderFrame() {
	const plot = plotArea(640);
	const timestamps = [0, 3600, 7200];
	const yScale = valueScale(100, [plot.bottom, plot.y]);
	const yTicks = valueTicks(yScale);
	const { container } = render(
		<svg width={640} height={CHART_HEIGHT}>
			<ChartFrame
				plot={plot}
				yScale={yScale}
				yTicks={yTicks}
				xScale={bandScale(timestamps, plot)}
				xTickValues={timestamps}
				bucketSeconds={3600}
			/>
		</svg>,
	);
	return { container, plot, yTicks };
}

describe("ChartFrame", () => {
	test("the value axis is bare: no axis line, no tick marks, labels 6px left of the plot", () => {
		const { container, plot, yTicks } = renderFrame();

		const axis = container.querySelector(".visx-axis-left") as SVGGElement;
		expect(axis.getAttribute("transform")).toBe(`translate(${plot.x}, 0)`);
		expect(axis.querySelector("line")).toBeNull();

		const labels = Array.from(axis.querySelectorAll("text"));
		expect(labels.map((label) => label.textContent)).toEqual(yTicks.map(String));
		for (const label of labels) {
			expect(label.getAttribute("x")).toBe("0");
			expect(label.getAttribute("dx")).toBe("-6px");
			expect(label.getAttribute("text-anchor")).toBe("end");
			// The label sits on the tick, centred: visx's own 0.25em nudge is
			// cancelled so it does not double up with the middle baseline.
			expect(label.getAttribute("dy")).toBe("0");
			expect(label.getAttribute("dominant-baseline")).toBe("middle");
		}
	});

	/**
	 * The time axis keeps its baseline and drops its tick marks, and the labels
	 * are placed by the group transform plus `dy` rather than by the font-size
	 * guess visx would otherwise make.
	 */
	test("the time axis keeps only its baseline, and its labels sit 16px below it", () => {
		const { container, plot } = renderFrame();

		const axis = container.querySelector(".visx-axis-bottom") as SVGGElement;
		expect(axis.getAttribute("transform")).toBe(`translate(0, ${plot.bottom})`);

		const lines = Array.from(axis.querySelectorAll("line"));
		expect(lines).toHaveLength(1);
		expect(lines[0].getAttribute("class")).toContain("visx-axis-line");

		for (const label of Array.from(axis.querySelectorAll("text"))) {
			expect(label.getAttribute("y")).toBe("0");
			expect(label.getAttribute("dy")).toBe("16px");
			expect(label.getAttribute("text-anchor")).toBe("middle");
		}
	});

	test("every grid line has a labelled tick on it and no tick floats without a line", () => {
		const { container } = renderFrame();

		const gridY = Array.from(container.querySelectorAll(".visx-rows line")).map((line) => line.getAttribute("y1"));
		const labelY = Array.from(container.querySelectorAll(".visx-axis-left text")).map((label) =>
			label.getAttribute("y"),
		);
		expect(gridY.length).toBeGreaterThan(0);
		expect(gridY).toEqual(labelY);
	});
});
