/**
 * Tests for the cleanup subsystems.
 *
 * Note: runSystemCleanup() is not tested directly here because it imports
 * from @emdash-cms/auth/adapters/kysely, which requires the auth package to
 * be built. Instead, we test each subsystem independently:
 * - cleanupExpiredChallenges: tested in auth/challenge-store.test.ts
 * - deleteExpiredTokens: tested below using direct DB operations
 * - cleanupPendingUploads: tested below via MediaRepository
 * - pruneOldRevisions: tested below via RevisionRepository
 */

import type { Kysely } from "kysely";
import { ulid } from "ulidx";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

import { MediaRepository } from "../../src/database/repositories/media.js";
import { RevisionRepository } from "../../src/database/repositories/revision.js";
import type { Database } from "../../src/database/types.js";
import { setupTestDatabase, setupTestDatabaseWithCollections } from "../utils/test-db.js";

describe("Revision Pruning", () => {
	let db: Kysely<Database>;
	let revisionRepo: RevisionRepository;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		revisionRepo = new RevisionRepository(db);
	});

	afterEach(async () => {
		await db.destroy();
	});

	it("prunes old revisions keeping the most recent N", async () => {
		const entryId = ulid();

		// Create a content entry
		const { sql } = await import("kysely");
		await sql`
			INSERT INTO ec_post (id, slug, status, created_at, updated_at, version)
			VALUES (${entryId}, ${"test-post"}, ${"draft"}, ${new Date().toISOString()}, ${new Date().toISOString()}, ${1})
		`.execute(db);

		// Create 200 revisions
		for (let i = 0; i < 200; i++) {
			await revisionRepo.create({
				collection: "post",
				entryId,
				data: { title: `Version ${i + 1}` },
			});
		}

		const countBefore = await revisionRepo.countByEntry("post", entryId);
		expect(countBefore).toBe(200);

		// Prune to keep 50
		const pruned = await revisionRepo.pruneOldRevisions("post", entryId, 50);

		expect(pruned).toBe(150);

		const countAfter = await revisionRepo.countByEntry("post", entryId);
		expect(countAfter).toBe(50);

		// Verify the remaining 50 are the newest
		const remaining = await revisionRepo.findByEntry("post", entryId);
		expect(remaining[0]?.data.title).toBe("Version 200");
		expect(remaining[49]?.data.title).toBe("Version 151");
	});

	it("is a no-op when revision count is at or below keepCount", async () => {
		const entryId = ulid();

		const { sql } = await import("kysely");
		await sql`
			INSERT INTO ec_post (id, slug, status, created_at, updated_at, version)
			VALUES (${entryId}, ${"test-post-2"}, ${"draft"}, ${new Date().toISOString()}, ${new Date().toISOString()}, ${1})
		`.execute(db);

		// Create 10 revisions
		for (let i = 0; i < 10; i++) {
			await revisionRepo.create({
				collection: "post",
				entryId,
				data: { title: `Version ${i + 1}` },
			});
		}

		const pruned = await revisionRepo.pruneOldRevisions("post", entryId, 50);
		expect(pruned).toBe(0);

		const countAfter = await revisionRepo.countByEntry("post", entryId);
		expect(countAfter).toBe(10);
	});
});

describe("MediaRepository.cleanupPendingUploads", () => {
	let db: Kysely<Database>;
	let mediaRepo: MediaRepository;

	beforeEach(async () => {
		db = await setupTestDatabase();
		mediaRepo = new MediaRepository(db);
	});

	afterEach(async () => {
		await db.destroy();
	});

	it("deletes pending uploads older than the default 1 hour", async () => {
		vi.useFakeTimers();

		// Create pending uploads
		for (let i = 0; i < 10; i++) {
			await mediaRepo.createPending({
				filename: `pending-${i}.jpg`,
				mimeType: "image/jpeg",
				storageKey: `uploads/pending-${i}.jpg`,
			});
		}

		// Advance past 1 hour
		vi.advanceTimersByTime(61 * 60 * 1000);

		const deletedKeys = await mediaRepo.cleanupPendingUploads();
		expect(deletedKeys).toHaveLength(10);
		// Verify actual storage keys are returned
		for (let i = 0; i < 10; i++) {
			expect(deletedKeys).toContain(`uploads/pending-${i}.jpg`);
		}

		vi.useRealTimers();
	});

	it("does not delete recent pending uploads", async () => {
		// Create pending uploads (current time -- not yet expired)
		for (let i = 0; i < 5; i++) {
			await mediaRepo.createPending({
				filename: `recent-${i}.jpg`,
				mimeType: "image/jpeg",
				storageKey: `uploads/recent-${i}.jpg`,
			});
		}

		const deletedKeys = await mediaRepo.cleanupPendingUploads();
		expect(deletedKeys).toHaveLength(0);
	});

	it("does not delete ready or failed items", async () => {
		vi.useFakeTimers();

		// Create items with different statuses
		await mediaRepo.create({
			filename: "ready.jpg",
			mimeType: "image/jpeg",
			storageKey: "uploads/ready.jpg",
			status: "ready",
		});

		const pending = await mediaRepo.createPending({
			filename: "pending.jpg",
			mimeType: "image/jpeg",
			storageKey: "uploads/pending.jpg",
		});
		await mediaRepo.markFailed(pending.id);

		// Advance past 1 hour
		vi.advanceTimersByTime(61 * 60 * 1000);

		const deletedKeys = await mediaRepo.cleanupPendingUploads();
		expect(deletedKeys).toHaveLength(0); // failed + ready should not be deleted

		vi.useRealTimers();

		const remaining = await db.selectFrom("media").select("id").execute();
		expect(remaining).toHaveLength(2);
	});

	it("respects custom maxAgeMs parameter", async () => {
		vi.useFakeTimers();

		await mediaRepo.createPending({
			filename: "short-lived.jpg",
			mimeType: "image/jpeg",
			storageKey: "uploads/short-lived.jpg",
		});

		// Advance 10 minutes
		vi.advanceTimersByTime(10 * 60 * 1000);

		// Cleanup with 5 min max age
		const deletedKeys = await mediaRepo.cleanupPendingUploads(5 * 60 * 1000);
		expect(deletedKeys).toHaveLength(1);
		expect(deletedKeys[0]).toBe("uploads/short-lived.jpg");

		vi.useRealTimers();
	});
});

describe("Expired token cleanup", () => {
	let db: Kysely<Database>;

	beforeEach(async () => {
		db = await setupTestDatabase();
	});

	afterEach(async () => {
		await db.destroy();
	});

	it("deletes expired tokens while keeping valid ones", async () => {
		const now = new Date();
		const expired = new Date(now.getTime() - 60 * 1000).toISOString(); // 1 min ago

		// Create a test user first (tokens reference users)
		const userId = ulid();
		await db
			.insertInto("users")
			.values({
				id: userId,
				email: "test@example.com",
				name: "Test",
				avatar_url: null,
				role: 50,
				email_verified: 1,
				disabled: 0,
				data: null,
				created_at: now.toISOString(),
				updated_at: now.toISOString(),
			})
			.execute();

		// Create 100 expired tokens
		for (let i = 0; i < 100; i++) {
			await db
				.insertInto("auth_tokens")
				.values({
					hash: `expired-hash-${i}`,
					user_id: userId,
					email: "test@example.com",
					type: "magic_link",
					role: null,
					invited_by: null,
					expires_at: expired,
					created_at: now.toISOString(),
				})
				.execute();
		}

		// Create 5 valid tokens
		const validExpiry = new Date(now.getTime() + 15 * 60 * 1000).toISOString();
		for (let i = 0; i < 5; i++) {
			await db
				.insertInto("auth_tokens")
				.values({
					hash: `valid-hash-${i}`,
					user_id: userId,
					email: "test@example.com",
					type: "magic_link",
					role: null,
					invited_by: null,
					expires_at: validExpiry,
					created_at: now.toISOString(),
				})
				.execute();
		}

		// Use the DB directly to simulate what deleteExpiredTokens does
		await db.deleteFrom("auth_tokens").where("expires_at", "<", new Date().toISOString()).execute();

		// Verify only valid ones remain
		const remaining = await db.selectFrom("auth_tokens").select("hash").execute();

		expect(remaining).toHaveLength(5);
		expect(remaining.every((r) => r.hash.startsWith("valid-"))).toBe(true);
	});
});
