import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import cookie from "@fastify/cookie";
import jwt from "@fastify/jwt";
import { migrate } from "drizzle-orm/libsql/migrator";
import Fastify, { type FastifyInstance } from "fastify";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runAlterMigrations } from "../db/db-utils.js";
import { documentationSchemaAjv } from "../utils/documentation-schema-keywords.js";

const { testClient, testDb, mockedEnv } = vi.hoisted(() => {
	const { createClient } = require("@libsql/client");
	const { drizzle } = require("drizzle-orm/libsql");
	const client = createClient({ url: ":memory:" });
	const db = drizzle(client);

	return {
		testClient: client,
		testDb: db,
		mockedEnv: {
			AUTH_ENABLED: true,
			REGISTRATION_ENABLED: true,
			FORM_LOGIN_ENABLED: true,
			OIDC_ENABLED: false,
			OIDC_PROVIDER_NAME: "SSO",
			NODE_ENV: "test",
			LOG_LEVEL: "silent",
			PORT: 3000,
			CORS_ORIGINS: "*",
			JWT_SECRET: "test-jwt-secret",
			REFRESH_SECRET: "test-refresh-secret",
			COOKIE_SECRET: "test-cookie-secret",
			ACCESS_TOKEN_TTL_MINUTES: 15,
			REFRESH_TOKEN_TTL_DAYS: 7,
			OPENAPI_DOCS_ENABLED: false,
		},
	};
});

vi.mock("../db/client.js", () => ({
	db: testDb,
	migrationsReady: Promise.resolve(),
}));

vi.mock("../plugins/env.js", () => ({ env: mockedEnv }));

const { doseRoutes } = await import("../routes/doses.js");

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const migrationsFolder = resolve(__dirname, "../../drizzle");

async function clearTables() {
	await testClient.execute("DELETE FROM dose_tracking");
	await testClient.execute("DELETE FROM share_tokens");
	await testClient.execute("DELETE FROM api_keys");
	await testClient.execute("DELETE FROM refresh_tokens");
	await testClient.execute("DELETE FROM medications");
	await testClient.execute("DELETE FROM user_settings");
	await testClient.execute("DELETE FROM users");
}

async function createUser(username: string) {
	const result = await testClient.execute({
		sql: "INSERT INTO users (username, auth_provider, is_active) VALUES (?, 'local', 1) RETURNING id",
		args: [username],
	});

	return Number(result.rows[0].id);
}

async function insertMedication(options: {
	id: number;
	userId: number;
	takenBy?: string[];
	packCount?: number;
	looseTablets?: number;
	start?: string;
}) {
	const intakeStart = options.start ?? "2025-01-01T08:00:00.000Z";
	await testClient.execute({
		sql: `INSERT INTO medications (
			id, user_id, name, taken_by_json, medication_form, package_type,
			pack_count, blisters_per_pack, pills_per_blister, loose_tablets, stock_adjustment,
			usage_json, every_json, start_json, intakes_json, intake_reminders_enabled
		) VALUES (?, ?, 'Test Medication', ?, 'tablet', 'blister', ?, 1, 10, ?, 0, '[1]', '[1]', ?, '[]', 0)`,
		args: [
			options.id,
			options.userId,
			JSON.stringify(options.takenBy ?? []),
			options.packCount ?? 1,
			options.looseTablets ?? 0,
			intakeStart,
			"[]",
		],
	});
}

async function insertUserSettings(userId: number, stockCalculationMode: "automatic" | "manual" = "automatic") {
	await testClient.execute({
		sql: "INSERT INTO user_settings (user_id, stock_calculation_mode) VALUES (?, ?)",
		args: [userId, stockCalculationMode],
	});
}

async function _insertShareToken(userId: number, token: string, takenBy: string) {
	await testClient.execute({
		sql: "INSERT INTO share_tokens (user_id, token, taken_by, schedule_days) VALUES (?, ?, ?, 30)",
		args: [userId, token, takenBy],
	});
}

function buildSessionCookie(app: FastifyInstance, userId: number, username: string) {
	const token = app.jwt.sign({ sub: userId, username });
	return `access_token=${token}`;
}

async function insertDose(options: {
	userId: number;
	doseId: string;
	markedBy?: string | null;
	dismissed?: boolean;
	takenAt?: number | null;
	takenSource?: "manual" | "automatic";
}) {
	await testClient.execute({
		sql: `INSERT INTO dose_tracking (user_id, dose_id, marked_by, dismissed, taken_at, taken_source)
		      VALUES (?, ?, ?, ?, ?, ?)`,
		args: [
			options.userId,
			options.doseId,
			options.markedBy ?? null,
			options.dismissed ? 1 : 0,
			options.takenAt === undefined ? Math.floor(Date.now() / 1000) : (options.takenAt ?? 0),
			options.takenSource ?? "manual",
		],
	});
}

describe("Dose Tracking API", () => {
	let app: FastifyInstance;
	let userId: number;
	let cookieHeader: string;

	beforeAll(async () => {
		await migrate(testDb, { migrationsFolder });
		await runAlterMigrations(testClient);

		app = Fastify({ logger: false, ajv: documentationSchemaAjv });
		await app.register(cookie, { secret: "test-cookie-secret" });
		await app.register(jwt, {
			secret: "test-jwt-secret",
			cookie: { cookieName: "access_token", signed: false },
		});
		await app.register(doseRoutes);
		await app.ready();
	});

	afterAll(async () => {
		await app.close();
		testClient.close();
	});

	beforeEach(async () => {
		await clearTables();
		userId = await createUser("dose-test-user");
		cookieHeader = buildSessionCookie(app, userId, "dose-test-user");
	});

	describe("POST /doses/taken", () => {
		it("marks a dose as taken", async () => {
			const doseId = "1-0-1735344000000";

			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: { doseId },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true });

			const result = await testClient.execute({
				sql: "SELECT dose_id, marked_by, taken_source FROM dose_tracking WHERE user_id = ? AND dose_id = ?",
				args: [userId, doseId],
			});
			expect(result.rows).toEqual([
				expect.objectContaining({ dose_id: doseId, marked_by: null, taken_source: "manual" }),
			]);
		});

		it("returns an idempotent response when the dose is already marked", async () => {
			const doseId = "1-0-1735344000000";
			await insertDose({ userId, doseId });

			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: { doseId },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true, message: "Already marked" });

			const countResult = await testClient.execute({
				sql: "SELECT COUNT(*) AS count FROM dose_tracking WHERE user_id = ? AND dose_id = ?",
				args: [userId, doseId],
			});
			expect(Number(countResult.rows[0].count)).toBe(1);
		});

		it("rejects requests without a doseId", async () => {
			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: {},
			});

			expect(response.statusCode).toBe(400);
			expect(response.json()).toEqual({ error: "Required" });
		});

		it("accepts dose IDs with a person suffix and special characters", async () => {
			const doseId = "5-0-1735344000000-Max Müller";

			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: { doseId },
			});

			expect(response.statusCode).toBe(200);

			const getResponse = await app.inject({
				method: "GET",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
			});

			expect(getResponse.statusCode).toBe(200);
			expect(getResponse.json().doses[0].doseId).toBe(doseId);
		});

		it("rejects taking a dose when the medication is out of stock", async () => {
			await insertMedication({ id: 5, userId, packCount: 0, looseTablets: 0 });
			await insertUserSettings(userId, "automatic");

			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: { doseId: "5-0-1735344000000" },
			});

			expect(response.statusCode).toBe(409);
			expect(response.json()).toEqual({ error: "Medication is out of stock", code: "OUT_OF_STOCK" });
		});

		it("allows taking a historical dose when stock existed at that occurrence", async () => {
			await insertMedication({
				id: 6,
				userId,
				packCount: 1,
				looseTablets: 0,
				start: "2025-01-01T08:00:00.000Z",
			});
			await insertUserSettings(userId, "automatic");

			const historicalDoseId = "6-0-1736064000000";
			const response = await app.inject({
				method: "POST",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
				payload: { doseId: historicalDoseId },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true });
		});
	});

	describe("GET /doses/taken", () => {
		it("returns an empty array when no doses were taken", async () => {
			const response = await app.inject({
				method: "GET",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ doses: [] });
		});

		it("returns only the authenticated user's taken doses with metadata", async () => {
			const otherUserId = await createUser("dose-other-user");
			await insertDose({
				userId,
				doseId: "1-0-1735344000000",
				markedBy: "Daniel",
				takenSource: "automatic",
			});
			await insertDose({ userId, doseId: "1-0-1735430400000" });
			await insertDose({ userId: otherUserId, doseId: "9-0-1735516800000" });

			const response = await app.inject({
				method: "GET",
				url: "/doses/taken",
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);
			const data = response.json();
			expect(data.doses).toHaveLength(2);
			expect(data.doses.map((dose: { doseId: string }) => dose.doseId).sort()).toEqual([
				"1-0-1735344000000",
				"1-0-1735430400000",
			]);
			expect(data.doses).toEqual(
				expect.arrayContaining([
					expect.objectContaining({ markedBy: "Daniel", takenSource: "automatic" }),
					expect.objectContaining({ markedBy: null, takenSource: "manual" }),
				])
			);
		});
	});

	describe("DELETE /doses/taken/:doseId", () => {
		it("unmarks an existing dose", async () => {
			const doseId = "1-0-1735344000000";
			await insertDose({ userId, doseId });

			const response = await app.inject({
				method: "DELETE",
				url: `/doses/taken/${encodeURIComponent(doseId)}`,
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true });

			const countResult = await testClient.execute({
				sql: "SELECT COUNT(*) AS count FROM dose_tracking WHERE user_id = ? AND dose_id = ?",
				args: [userId, doseId],
			});
			expect(Number(countResult.rows[0].count)).toBe(0);
		});

		it("keeps the record when the dose is dismissed", async () => {
			const doseId = "1-0-1735344000000";
			await insertDose({ userId, doseId, dismissed: true });

			const response = await app.inject({
				method: "DELETE",
				url: `/doses/taken/${encodeURIComponent(doseId)}`,
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);

			const result = await testClient.execute({
				sql: "SELECT dose_id, dismissed FROM dose_tracking WHERE user_id = ? AND dose_id = ?",
				args: [userId, doseId],
			});
			expect(result.rows).toEqual([expect.objectContaining({ dose_id: doseId, dismissed: 1 })]);
		});

		it("still succeeds when the dose does not exist", async () => {
			const response = await app.inject({
				method: "DELETE",
				url: "/doses/taken/nonexistent-dose-id",
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true });
		});
	});

	describe("POST /doses/dismiss", () => {
		it("dismisses multiple doses", async () => {
			const response = await app.inject({
				method: "POST",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
				payload: { doseIds: ["1-0-1735344000000", "1-0-1735430400000"] },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true, dismissedCount: 2 });

			const result = await testClient.execute({
				sql: "SELECT COUNT(*) AS count FROM dose_tracking WHERE user_id = ? AND dismissed = 1",
				args: [userId],
			});
			expect(Number(result.rows[0].count)).toBe(2);
		});

		it("does not double-count already dismissed doses", async () => {
			const doseId = "1-0-1735344000000";
			await insertDose({ userId, doseId, dismissed: true });

			const response = await app.inject({
				method: "POST",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
				payload: { doseIds: [doseId] },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true, dismissedCount: 0 });
		});

		it("converts a taken dose into a dismissed one", async () => {
			const doseId = "1-0-1735344000000";
			await insertDose({ userId, doseId, dismissed: false });

			const response = await app.inject({
				method: "POST",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
				payload: { doseIds: [doseId] },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true, dismissedCount: 1 });

			const result = await testClient.execute({
				sql: "SELECT dismissed FROM dose_tracking WHERE user_id = ? AND dose_id = ?",
				args: [userId, doseId],
			});
			expect(result.rows).toEqual([expect.objectContaining({ dismissed: 1 })]);
		});

		it("rejects missing or empty doseIds", async () => {
			const emptyResponse = await app.inject({
				method: "POST",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
				payload: { doseIds: [] },
			});

			expect(emptyResponse.statusCode).toBe(400);
			expect(emptyResponse.json()).toEqual({ error: "At least one doseId is required" });

			const missingResponse = await app.inject({
				method: "POST",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
				payload: {},
			});

			expect(missingResponse.statusCode).toBe(400);
			expect(missingResponse.json()).toEqual({ error: "Required" });
		});
	});

	describe("DELETE /doses/dismiss", () => {
		it("clears dismissed-only records and removes the dismissed flag from taken doses", async () => {
			await insertDose({ userId, doseId: "1-0-1735344000000", dismissed: true, takenAt: null });
			await insertDose({ userId, doseId: "1-0-1735430400000", dismissed: true, markedBy: "Daniel" });

			const response = await app.inject({
				method: "DELETE",
				url: "/doses/dismiss",
				headers: { cookie: cookieHeader },
			});

			expect(response.statusCode).toBe(200);
			expect(response.json()).toEqual({ success: true, clearedCount: 2 });

			const rows = await testClient.execute({
				sql: "SELECT dose_id, dismissed, marked_by FROM dose_tracking WHERE user_id = ? ORDER BY dose_id ASC",
				args: [userId],
			});
			expect(rows.rows).toEqual([
				expect.objectContaining({ dose_id: "1-0-1735430400000", dismissed: 0, marked_by: "Daniel" }),
			]);
		});
	});
});
