import test from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { setup, signup } from "./helpers.js";
import { computeTotalScore, SCORE_ATTRS, TICK_ATTRS } from "../shared/cupping.js";

async function bootstrapPlan(agent, csrf) {
	const r = await agent
		.post("/api/plans")
		.set("x-csrf-token", csrf)
		.send({ plan: { fields: { 0.1: "Test coffee" } } });
	return r.body.plan.id;
}

test("cupping: auth and CSRF are required on every route", async () => {
	const { app } = await setup();
	const anon = request.agent(app);
	assert.equal((await anon.get("/api/cupping")).status, 401);
	assert.equal((await anon.post("/api/cupping").send({})).status, 401);

	const agent = request.agent(app);
	const { csrf } = await signup(agent, "owner@example.com");
	assert.equal((await agent.post("/api/cupping").send({})).status, 403);
	const created = await agent.post("/api/cupping").set("x-csrf-token", csrf).send({});
	assert.equal(created.status, 201);
	assert.equal(
		(await agent.put(`/api/cupping/${created.body.session.id}`).send({ data: {} })).status,
		403,
	);
	assert.equal((await agent.delete(`/api/cupping/${created.body.session.id}`)).status, 403);
});

test("cupping: create (with and without a linked plan), list, and ownership isolation", async () => {
	const { app } = await setup();
	const first = request.agent(app);
	const second = request.agent(app);
	const { csrf: firstCsrf } = await signup(first, "one@example.com");
	await signup(second, "two@example.com");
	const planId = await bootstrapPlan(first, firstCsrf);

	const linked = await first
		.post("/api/cupping")
		.set("x-csrf-token", firstCsrf)
		.send({ roastPlanId: planId, cupCount: 4 });
	assert.equal(linked.status, 201);
	assert.equal(linked.body.session.roastPlanId, planId);
	assert.equal(linked.body.session.data.cup_count, 4);
	assert.equal(linked.body.session.totalScore, 0);

	const unlinked = await first.post("/api/cupping").set("x-csrf-token", firstCsrf).send({});
	assert.equal(unlinked.status, 201);
	assert.equal(unlinked.body.session.roastPlanId, null);

	assert.equal((await first.get("/api/cupping")).body.sessions.length, 2);
	assert.equal((await second.get("/api/cupping")).body.sessions.length, 0);
	assert.equal((await second.get(`/api/cupping/${linked.body.session.id}`)).status, 404);

	const filtered = await first.get(`/api/cupping?plan=${planId}`);
	assert.equal(filtered.body.sessions.length, 1);
	assert.equal(filtered.body.sessions[0].id, linked.body.session.id);
});

test("cupping: creating against another user's plan is refused", async () => {
	const { app } = await setup();
	const owner = request.agent(app);
	const attacker = request.agent(app);
	const { csrf: ownerCsrf } = await signup(owner, "owner@example.com");
	const { csrf: attackerCsrf } = await signup(attacker, "attacker@example.com");
	const planId = await bootstrapPlan(owner, ownerCsrf);
	const attempt = await attacker
		.post("/api/cupping")
		.set("x-csrf-token", attackerCsrf)
		.send({ roastPlanId: planId });
	assert.equal(attempt.status, 404);
});

test("cupping: server always recomputes the total score and ignores a client-supplied value", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	const { csrf } = await signup(agent, "taster@example.com");
	const session = (
		await agent.post("/api/cupping").set("x-csrf-token", csrf).send({ cupCount: 5 })
	).body.session;

	const scores = Object.fromEntries(SCORE_ATTRS.map((a) => [a, 8]));
	const ticks = Object.fromEntries(TICK_ATTRS.map((a) => [a, 5]));
	const expected = computeTotalScore(scores, ticks, 1, 1, 5);

	const saved = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({
			data: {
				cup_count: 5,
				scores,
				ticks,
				taint_cups: 1,
				fault_cups: 1,
				flavor_tags: ["fruity.berry.blueberry"],
				notes: "Bright and clean.",
				total_score: 999999, // must be discarded — the server recomputes it
			},
		});
	assert.equal(saved.status, 200);
	assert.equal(saved.body.session.totalScore, expected);
	assert.notEqual(expected, 999999);

	const reloaded = await agent.get(`/api/cupping/${session.id}`);
	assert.equal(reloaded.body.session.totalScore, expected);
	assert.deepEqual(reloaded.body.session.data.flavor_tags, ["fruity.berry.blueberry"]);
});

test("cupping: coerceSession validation errors surface as 400s", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	const { csrf } = await signup(agent, "taster@example.com");
	const session = (await agent.post("/api/cupping").set("x-csrf-token", csrf).send({})).body
		.session;

	const badFlavor = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({ data: { flavor_tags: ["Not A Valid Id!"] } });
	assert.equal(badFlavor.status, 400);
	assert.equal(badFlavor.body.code, "bad_session");

	const dedupedTags = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({ data: { flavor_tags: Array.from({ length: 33 }, () => "other.chemical.rubber") } });
	// 33 identical tags dedupe to 1, so this specific input should NOT throw — verifies dedup
	// happens before the >32 check.
	assert.equal(dedupedTags.status, 200);
	assert.deepEqual(dedupedTags.body.session.data.flavor_tags, ["other.chemical.rubber"]);

	// Build 33 genuinely distinct valid-shaped ids by walking the real taxonomy, to trip the
	// actual >32-after-dedup limit.
	const { FLAVOR_TAXONOMY } = await import("../shared/cupping.js");
	const manyValid = [];
	for (const [famId, fam] of Object.entries(FLAVOR_TAXONOMY)) {
		for (const [subId, descriptors] of Object.entries(fam.subgroups)) {
			for (const d of descriptors) manyValid.push(`${famId}.${subId}.${d}`);
		}
	}
	assert.ok(manyValid.length > 32);
	const overLimit = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({ data: { flavor_tags: manyValid } });
	assert.equal(overLimit.status, 400);

	const badStage = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({ data: { stage_marks: [{ stage: "not_a_real_stage", elapsed_sec: 1 }] } });
	assert.equal(badStage.status, 400);

	const notObject = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({ data: "nope" });
	assert.equal(notObject.status, 400);
});

test("cupping: scores clamp/snap and ticks clamp to cup count", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	const { csrf } = await signup(agent, "taster@example.com");
	const session = (
		await agent.post("/api/cupping").set("x-csrf-token", csrf).send({ cupCount: 3 })
	).body.session;

	const saved = await agent
		.put(`/api/cupping/${session.id}`)
		.set("x-csrf-token", csrf)
		.send({
			data: {
				cup_count: 3,
				scores: { flavor: 11, acidity: 7.13, body: -1 },
				ticks: { uniformity: 99 },
			},
		});
	assert.equal(saved.status, 200);
	assert.equal(saved.body.session.data.scores.flavor, 10);
	assert.equal(saved.body.session.data.scores.acidity, 7.25);
	assert.equal(saved.body.session.data.scores.body, 0);
	assert.equal(saved.body.session.data.ticks.uniformity, 3);
});

test("cupping: delete removes the session, and deleting the linked plan nulls roastPlanId instead of deleting the session", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	const { csrf } = await signup(agent, "taster@example.com");
	const planId = await bootstrapPlan(agent, csrf);
	const session = (
		await agent
			.post("/api/cupping")
			.set("x-csrf-token", csrf)
			.send({ roastPlanId: planId })
	).body.session;

	await agent.delete(`/api/plans/${planId}`).set("x-csrf-token", csrf);
	const afterPlanDelete = await agent.get(`/api/cupping/${session.id}`);
	assert.equal(afterPlanDelete.status, 200);
	assert.equal(afterPlanDelete.body.session.roastPlanId, null);

	assert.equal(
		(await agent.delete(`/api/cupping/${session.id}`).set("x-csrf-token", csrf)).status,
		200,
	);
	assert.equal((await agent.get(`/api/cupping/${session.id}`)).status, 404);
});
