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

import { ContentRepository } from "../../../src/database/repositories/content.js";
import type { Database } from "../../../src/database/types.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
import { applySeed } from "../../../src/seed/apply.js";
import type { SeedFile } from "../../../src/seed/types.js";
import type { Storage, UploadOptions } from "../../../src/storage/types.js";
import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js";

// Regex patterns for file validation
const PNG_EXTENSION_REGEX = /\.png$/;

// Mock fetch globally
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

// Create a mock storage that tracks uploads
function createMockStorage(): Storage & { uploads: UploadOptions[] } {
	const uploads: UploadOptions[] = [];

	return {
		uploads,
		async upload(options: UploadOptions): Promise<void> {
			uploads.push(options);
		},
		async download(key: string): Promise<{ body: Uint8Array; contentType: string }> {
			const upload = uploads.find((u) => u.key === key);
			if (!upload) throw new Error(`Not found: ${key}`);
			return { body: upload.body, contentType: upload.contentType };
		},
		async delete(key: string): Promise<void> {
			const index = uploads.findIndex((u) => u.key === key);
			if (index >= 0) uploads.splice(index, 1);
		},
		async exists(key: string): Promise<boolean> {
			return uploads.some((u) => u.key === key);
		},
		getPublicUrl(key: string): string {
			return `https://storage.example.com/${key}`;
		},
	};
}

// Create a mock response for fetch
function createMockResponse(
	body: Uint8Array,
	contentType: string,
	ok = true,
	status = 200,
): Response {
	return {
		ok,
		status,
		headers: new Headers({ "content-type": contentType }),
		arrayBuffer: async () => body.buffer,
	} as Response;
}

// Simple 1x1 PNG for testing
const MOCK_PNG = new Uint8Array([
	0x89,
	0x50,
	0x4e,
	0x47,
	0x0d,
	0x0a,
	0x1a,
	0x0a, // PNG signature
	0x00,
	0x00,
	0x00,
	0x0d, // IHDR length
	0x49,
	0x48,
	0x44,
	0x52, // IHDR chunk type
	0x00,
	0x00,
	0x00,
	0x01, // width: 1
	0x00,
	0x00,
	0x00,
	0x01, // height: 1
	0x08,
	0x02,
	0x00,
	0x00,
	0x00, // bit depth, color type, etc.
	0x90,
	0x77,
	0x53,
	0xde, // CRC
]);

// Simple 1x1 JPEG for testing
const MOCK_JPEG = new Uint8Array([
	0xff,
	0xd8,
	0xff,
	0xe0, // SOI + APP0
	0x00,
	0x10, // APP0 length
	0x4a,
	0x46,
	0x49,
	0x46,
	0x00, // JFIF identifier
	0x01,
	0x01, // version
	0x00, // aspect ratio units
	0x00,
	0x01, // X density (1)
	0x00,
	0x01, // Y density (1)
	0x00,
	0x00, // thumbnail dimensions
	0xff,
	0xd9, // EOI
]);

describe("$media seed resolution", () => {
	let db: Kysely<Database>;
	let storage: Storage & { uploads: UploadOptions[] };

	beforeEach(async () => {
		db = await setupTestDatabase();
		storage = createMockStorage();
		mockFetch.mockReset();

		// Set up a collection with an image field
		const registry = new SchemaRegistry(db);
		await registry.createCollection({ slug: "posts", label: "Posts" });
		await registry.createField("posts", {
			slug: "title",
			label: "Title",
			type: "string",
		});
		await registry.createField("posts", {
			slug: "featured_image",
			label: "Featured Image",
			type: "image",
		});
	});

	afterEach(async () => {
		await teardownTestDatabase(db);
		vi.restoreAllMocks();
	});

	it("should resolve $media references by downloading and uploading", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(MOCK_PNG, "image/png"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello World",
							featured_image: {
								$media: {
									url: "https://example.com/image.png",
									alt: "Test image",
									filename: "my-image.png",
								},
							},
						},
					},
				],
			},
		};

		const result = await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "https://mysite.com",
		});

		expect(result.media.created).toBe(1);
		expect(result.content.created).toBe(1);
		expect(storage.uploads).toHaveLength(1);

		// Check the upload
		expect(storage.uploads[0].contentType).toBe("image/png");
		expect(storage.uploads[0].key).toMatch(PNG_EXTENSION_REGEX);

		// Check the content has resolved ImageValue
		const contentRepo = new ContentRepository(db);
		const entry = await contentRepo.findBySlug("posts", "hello");

		// ImageValue stores id (URL is built at runtime by EmDashImage)
		expect(entry?.data.featured_image).toMatchObject({
			id: expect.any(String),
			alt: "Test image",
		});
	});

	it("should cache repeated $media URLs", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(MOCK_JPEG, "image/jpeg"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "first",
						data: {
							title: "First",
							featured_image: {
								$media: {
									url: "https://example.com/shared.jpg",
									alt: "Shared image",
								},
							},
						},
					},
					{
						id: "post-2",
						slug: "second",
						data: {
							title: "Second",
							featured_image: {
								$media: {
									url: "https://example.com/shared.jpg",
									alt: "Different alt text",
								},
							},
						},
					},
				],
			},
		};

		const result = await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		// Only downloaded/uploaded once
		expect(result.media.created).toBe(1);
		expect(result.media.skipped).toBe(1);
		expect(mockFetch).toHaveBeenCalledTimes(1);
		expect(storage.uploads).toHaveLength(1);

		// Both entries should have the same src but different alt
		const contentRepo = new ContentRepository(db);
		const first = await contentRepo.findBySlug("posts", "first");
		const second = await contentRepo.findBySlug("posts", "second");

		expect(first?.data.featured_image.src).toBe(second?.data.featured_image.src);
		expect(first?.data.featured_image.alt).toBe("Shared image");
		expect(second?.data.featured_image.alt).toBe("Different alt text");
	});

	it("should skip $media when storage is not configured", async () => {
		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/image.png",
									alt: "Test",
								},
							},
						},
					},
				],
			},
		};

		// No storage provided
		const result = await applySeed(db, seed, { includeContent: true });

		expect(result.media.skipped).toBe(1);
		expect(result.media.created).toBe(0);
		expect(mockFetch).not.toHaveBeenCalled();

		// Image field should be null/undefined (not resolved)
		const contentRepo = new ContentRepository(db);
		const entry = await contentRepo.findBySlug("posts", "hello");
		expect(entry?.data.featured_image).toBeFalsy();
	});

	it("should handle failed downloads gracefully", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(new Uint8Array(), "", false, 404));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/missing.png",
									alt: "Missing",
								},
							},
						},
					},
				],
			},
		};

		const result = await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		expect(result.media.skipped).toBe(1);
		expect(result.content.created).toBe(1);

		// Image field should be null/undefined (not resolved)
		const contentRepo = new ContentRepository(db);
		const entry = await contentRepo.findBySlug("posts", "hello");
		expect(entry?.data.featured_image).toBeFalsy();
	});

	it("should handle fetch errors gracefully", async () => {
		mockFetch.mockRejectedValueOnce(new Error("Network error"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/error.png",
									alt: "Error",
								},
							},
						},
					},
				],
			},
		};

		const result = await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		expect(result.media.skipped).toBe(1);
		expect(result.content.created).toBe(1);
	});

	it("should generate filename from URL when not specified", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(MOCK_PNG, "image/png"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/path/to/beautiful-sunset.png?size=large",
									alt: "Sunset",
								},
							},
						},
					},
				],
			},
		};

		await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		// Check media record in database
		const media = await db.selectFrom("media").selectAll().executeTakeFirst();

		expect(media?.filename).toBe("beautiful-sunset.png");
	});

	it("should use specified filename", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(MOCK_PNG, "image/png"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/random-id-12345.png",
									alt: "Custom",
									filename: "my-custom-name.png",
								},
							},
						},
					},
				],
			},
		};

		await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		const media = await db.selectFrom("media").selectAll().executeTakeFirst();

		expect(media?.filename).toBe("my-custom-name.png");
	});

	it("should create media record with correct metadata", async () => {
		mockFetch.mockResolvedValueOnce(createMockResponse(MOCK_PNG, "image/png"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							featured_image: {
								$media: {
									url: "https://example.com/test.png",
									alt: "Test alt text",
									caption: "Test caption",
									filename: "test-image.png",
								},
							},
						},
					},
				],
			},
		};

		await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		const media = await db.selectFrom("media").selectAll().executeTakeFirst();

		expect(media).toMatchObject({
			filename: "test-image.png",
			mime_type: "image/png",
			alt: "Test alt text",
			caption: "Test caption",
			status: "ready",
		});
		expect(media?.storage_key).toMatch(PNG_EXTENSION_REGEX);
	});

	it("should resolve nested $media in arrays", async () => {
		// Set up a collection with a json field for gallery
		const registry = new SchemaRegistry(db);
		await registry.createField("posts", {
			slug: "gallery",
			label: "Gallery",
			type: "json",
		});

		mockFetch
			.mockResolvedValueOnce(createMockResponse(MOCK_PNG, "image/png"))
			.mockResolvedValueOnce(createMockResponse(MOCK_JPEG, "image/jpeg"));

		const seed: SeedFile = {
			version: "1",
			content: {
				posts: [
					{
						id: "post-1",
						slug: "hello",
						data: {
							title: "Hello",
							gallery: [
								{
									$media: {
										url: "https://example.com/one.png",
										alt: "Image one",
									},
								},
								{
									$media: {
										url: "https://example.com/two.jpg",
										alt: "Image two",
									},
								},
							],
						},
					},
				],
			},
		};

		const result = await applySeed(db, seed, {
			includeContent: true,
			storage,
			baseUrl: "",
		});

		expect(result.media.created).toBe(2);

		const contentRepo = new ContentRepository(db);
		const entry = await contentRepo.findBySlug("posts", "hello");

		expect(entry?.data.gallery).toHaveLength(2);
		// ImageValue stores id (URL is built at runtime by EmDashImage)
		const gallery = entry?.data.gallery as unknown[] | undefined;
		expect(gallery?.[0]).toMatchObject({
			id: expect.any(String),
			alt: "Image one",
		});
		expect(gallery?.[1]).toMatchObject({
			id: expect.any(String),
			alt: "Image two",
		});
	});
});
