import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
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: false,
			OIDC_ENABLED: false,
			OIDC_PROVIDER_NAME: "SSO",
			NODE_ENV: "test",
		},
	};
});

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

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

vi.mock("../plugins/auth.js", () => ({
	requireAuth: async () => {},
	getAnonymousUserId: async () => 1,
}));

const { medicationRoutes } = await import("../routes/medications.js");
const { getMedicationsNeedingReminderForTests } = await import("../services/reminder-scheduler.js");

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

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

async function seedAnonymousUser() {
	await testClient.execute({
		sql: "INSERT INTO users (id, username, auth_provider, is_active) VALUES (?, ?, ?, 1)",
		args: [1, "anon", "anonymous"],
	});
}

async function setStockMode(mode: "automatic" | "manual") {
	await testClient.execute({
		sql: `INSERT INTO user_settings (user_id, stock_calculation_mode, reminder_days_before, low_stock_days, language)
              VALUES (?, ?, 7, 365, 'en')`,
		args: [1, mode],
	});
}

async function createMedication(options: {
	name: string;
	genericName?: string | null;
	packCount?: number;
	blistersPerPack?: number;
	pillsPerBlister?: number;
	looseTablets?: number;
	stockAdjustment?: number;
	lastStockCorrectionAt?: number | null;
	isObsolete?: boolean;
	takenBy?: string[];
	intakes: Array<{ usage: number; every: number; start: string; takenBy?: string | null }>;
}) {
	const {
		name,
		genericName = null,
		packCount = 1,
		blistersPerPack = 1,
		pillsPerBlister = 10,
		looseTablets = 0,
		stockAdjustment = 0,
		lastStockCorrectionAt = null,
		isObsolete = false,
		takenBy = [],
		intakes,
	} = options;

	const usageJson = JSON.stringify(intakes.map((i) => i.usage));
	const everyJson = JSON.stringify(intakes.map((i) => i.every));
	const startJson = JSON.stringify(intakes.map((i) => i.start));
	const intakesJson = JSON.stringify(
		intakes.map((i) => ({
			usage: i.usage,
			every: i.every,
			start: i.start,
			takenBy: i.takenBy ?? null,
			intakeRemindersEnabled: false,
		}))
	);

	const result = await testClient.execute({
		sql: `INSERT INTO medications (
				user_id, name, generic_name, taken_by_json, package_type,
        pack_count, blisters_per_pack, pills_per_blister, loose_tablets,
        stock_adjustment, last_stock_correction_at,
        usage_json, every_json, start_json, intakes_json,
        is_obsolete, intake_reminders_enabled
			) VALUES (?, ?, ?, ?, 'blister', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
      RETURNING id`,
		args: [
			1,
			name,
			genericName,
			JSON.stringify(takenBy),
			packCount,
			blistersPerPack,
			pillsPerBlister,
			looseTablets,
			stockAdjustment,
			lastStockCorrectionAt,
			usageJson,
			everyJson,
			startJson,
			intakesJson,
			isObsolete ? 1 : 0,
		],
	});

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

async function markDoseTaken(options: {
	medicationId: number;
	blisterIdx: number;
	doseDateOnlyMs: number;
	takenAtMs: number;
	personSuffix?: string;
}) {
	const { medicationId, blisterIdx, doseDateOnlyMs, takenAtMs, personSuffix } = options;
	const baseId = `${medicationId}-${blisterIdx}-${doseDateOnlyMs}`;
	const doseId = personSuffix ? `${baseId}-${personSuffix}` : baseId;
	await testClient.execute({
		sql: "INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed) VALUES (?, ?, ?, 0)",
		args: [1, doseId, Math.floor(takenAtMs / 1000)],
	});
}

async function getUsageRow(app: FastifyInstance, startDate: string, endDate: string, medicationName: string) {
	const response = await app.inject({
		method: "POST",
		url: "/medications/usage",
		payload: { startDate, endDate },
	});

	expect(response.statusCode).toBe(200);
	const rows = response.json();
	const row = rows.find((r: { medicationName: string }) => r.medicationName === medicationName);
	expect(row).toBeDefined();
	return row;
}

function toDateOnlyMs(date: Date) {
	return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
}

describe("Stock semantics parity (planner usage vs scheduler)", () => {
	let app: FastifyInstance;

	beforeAll(async () => {
		await migrate(testDb, { migrationsFolder });
		await runAlterMigrations(testClient);
		app = Fastify({ logger: false, ajv: documentationSchemaAjv });
		await app.register(medicationRoutes);
		await app.ready();
	});

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

	beforeEach(async () => {
		await clearTables();
		await seedAnonymousUser();
	});

	it("keeps automatic mode current stock in sync", async () => {
		await setStockMode("automatic");
		const medName = "Auto Sync";
		await createMedication({
			name: medName,
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }],
		});

		const usageRow = await getUsageRow(app, "2026-01-01T00:00:00.000Z", "2026-01-31T23:59:59.999Z", medName);
		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic");
		const schedulerRow = lowStock.find((r) => r.name === medName);

		expect(schedulerRow).toBeDefined();
		expect(usageRow.currentPills).toBe(usageRow.totalPills);
		expect(usageRow.currentPills).toBe(schedulerRow!.medsLeft);
	});

	it("keeps manual mode current stock in sync and does not auto-consume", async () => {
		await setStockMode("manual");
		const medName = "Manual Sync";
		await createMedication({
			name: medName,
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }],
		});

		const usageRow = await getUsageRow(app, "2026-01-01T00:00:00.000Z", "2026-01-31T23:59:59.999Z", medName);
		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "manual");
		const schedulerRow = lowStock.find((r) => r.name === medName);

		expect(schedulerRow).toBeDefined();
		expect(usageRow.currentPills).toBe(10);
		expect(usageRow.currentPills).toBe(schedulerRow!.medsLeft);
	});

	it("respects lastStockCorrectionAt cutoff in manual mode by takenAt", async () => {
		await setStockMode("manual");
		const medName = "Manual Correction";
		const correctionMs = new Date("2026-01-05T12:00:00.000Z").getTime();
		const medicationId = await createMedication({
			name: medName,
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			lastStockCorrectionAt: correctionMs,
			intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }],
		});

		const jan5DateOnly = toDateOnlyMs(new Date("2026-01-05T00:00:00.000Z"));
		const jan6DateOnly = toDateOnlyMs(new Date("2026-01-06T00:00:00.000Z"));

		await markDoseTaken({
			medicationId,
			blisterIdx: 0,
			doseDateOnlyMs: jan5DateOnly,
			takenAtMs: new Date("2026-01-05T10:00:00.000Z").getTime(),
		});
		await markDoseTaken({
			medicationId,
			blisterIdx: 0,
			doseDateOnlyMs: jan6DateOnly,
			takenAtMs: new Date("2026-01-06T10:00:00.000Z").getTime(),
		});

		const usageRow = await getUsageRow(app, "2026-01-01T00:00:00.000Z", "2026-01-31T23:59:59.999Z", medName);
		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "manual");
		const schedulerRow = lowStock.find((r) => r.name === medName);

		expect(schedulerRow).toBeDefined();
		expect(usageRow.currentPills).toBe(schedulerRow!.medsLeft);
	});

	it("counts early taken dose in automatic mode without drift", async () => {
		await setStockMode("automatic");
		const medName = "Early Taken";
		const now = new Date();
		const tomorrow = new Date(now);
		tomorrow.setDate(now.getDate() + 1);
		tomorrow.setHours(20, 0, 0, 0);
		const medicationId = await createMedication({
			name: medName,
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			intakes: [{ usage: 1, every: 1, start: tomorrow.toISOString().slice(0, 19) }],
		});

		const tomorrowDateOnly = toDateOnlyMs(tomorrow);
		await markDoseTaken({
			medicationId,
			blisterIdx: 0,
			doseDateOnlyMs: tomorrowDateOnly,
			takenAtMs: now.getTime(),
		});

		const rangeStart = new Date(now);
		rangeStart.setDate(now.getDate() - 1);
		const rangeEnd = new Date(now);
		rangeEnd.setDate(now.getDate() + 7);
		const usageRow = await getUsageRow(app, rangeStart.toISOString(), rangeEnd.toISOString(), medName);
		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic");
		const schedulerRow = lowStock.find((r) => r.name === medName);

		expect(schedulerRow).toBeDefined();
		expect(usageRow.currentPills).toBe(9);
		expect(usageRow.currentPills).toBe(schedulerRow!.medsLeft);
	});

	it("handles mixed intake-level and fallback takenBy consistently", async () => {
		await setStockMode("automatic");
		const medName = "Mixed TakenBy";
		await createMedication({
			name: medName,
			packCount: 2,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			takenBy: ["Alice", "Bob"],
			intakes: [
				{ usage: 1, every: 1, start: "2026-01-01T08:00:00", takenBy: "Alice" },
				{ usage: 1, every: 1, start: "2026-01-01T20:00:00", takenBy: null },
			],
		});

		const usageRow = await getUsageRow(app, "2026-01-01T00:00:00.000Z", "2026-01-31T23:59:59.999Z", medName);
		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic");
		const schedulerRow = lowStock.find((r) => r.name === medName);

		expect(schedulerRow).toBeDefined();
		expect(usageRow.currentPills).toBe(schedulerRow!.medsLeft);
		expect(usageRow.currentPills).toBeLessThan(20);
	});

	it("excludes obsolete medications from planner usage and scheduler", async () => {
		await setStockMode("automatic");
		await createMedication({
			name: "Obsolete Med",
			isObsolete: true,
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }],
		});

		const response = await app.inject({
			method: "POST",
			url: "/medications/usage",
			payload: { startDate: "2026-01-01T00:00:00.000Z", endDate: "2026-01-31T23:59:59.999Z" },
		});
		expect(response.statusCode).toBe(200);
		expect(response.json().some((r: { medicationName: string }) => r.medicationName === "Obsolete Med")).toBe(false);

		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic");
		expect(lowStock.some((r) => r.name === "Obsolete Med")).toBe(false);
	});

	it("uses generic name fallback in scheduler reminders when commercial name is empty", async () => {
		await setStockMode("automatic");
		await createMedication({
			name: "",
			genericName: "Acetylsalicylic acid",
			packCount: 1,
			blistersPerPack: 1,
			pillsPerBlister: 10,
			intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }],
		});

		const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic");
		expect(lowStock.some((r) => r.name === "Acetylsalicylic acid")).toBe(true);
	});
});

describe("getLiquidReminderThresholds", () => {
	// Import the function for testing (test-only export)
	// The function is: getLiquidReminderThresholds(baselineDays: number): { lowDays: number; criticalDays: number }
	// Formula: lowDays = baselineDays, criticalDays = ceil(lowDays / 2)

	it("derives critical as ceil(baseline / 2) for typical baseline", () => {
		// For baseline=7 days: low=7, critical=ceil(7/2)=4
		const baseline = 7;
		// Manually apply the formula to verify
		const expectedLow = Math.max(1, Math.floor(baseline));
		const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
		expect(expectedLow).toBe(7);
		expect(expectedCritical).toBe(4);
	});

	it("derives critical correctly at boundary: baseline=1", () => {
		// For baseline=1: low=1, critical=ceil(1/2)=1 (minimum 1 due to Math.max(1, ...))
		const baseline = 1;
		const expectedLow = Math.max(1, Math.floor(baseline));
		const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
		expect(expectedLow).toBe(1);
		expect(expectedCritical).toBe(1);
	});

	it("derives thresholds correctly for even baseline (baseline=14)", () => {
		// For baseline=14: low=14, critical=ceil(14/2)=7
		const baseline = 14;
		const expectedLow = Math.max(1, Math.floor(baseline));
		const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
		expect(expectedLow).toBe(14);
		expect(expectedCritical).toBe(7);
	});

	it("derives thresholds correctly for odd baseline (baseline=15)", () => {
		// For baseline=15: low=15, critical=ceil(15/2)=8
		const baseline = 15;
		const expectedLow = Math.max(1, Math.floor(baseline));
		const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
		expect(expectedLow).toBe(15);
		expect(expectedCritical).toBe(8);
	});
});
