/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {createAPIApp} from '@fluxer/api/src/App';
import {Config} from '@fluxer/api/src/Config';
import {clearSqliteStore} from '@fluxer/api/src/database/SqliteKV';
import {MeilisearchSearchProvider} from '@fluxer/api/src/infrastructure/MeilisearchSearchProvider';
import {NullSearchProvider} from '@fluxer/api/src/infrastructure/NullSearchProvider';
import {
	setInjectedBlueskyOAuthService,
	setInjectedGatewayService,
	setInjectedKVProvider,
	setInjectedMediaService,
	setInjectedS3Service,
	setInjectedSearchProviderService,
	setInjectedWorkerService,
} from '@fluxer/api/src/middleware/ServiceRegistry';
import type {ISearchProvider} from '@fluxer/api/src/search/ISearchProvider';
import {acquireMeilisearchTestServer} from '@fluxer/api/src/test/meilisearch/MeilisearchTestServer';
import {MockBlueskyOAuthService} from '@fluxer/api/src/test/mocks/MockBlueskyOAuthService';
import {MockKVProvider} from '@fluxer/api/src/test/mocks/MockKVProvider';
import {NoopLogger} from '@fluxer/api/src/test/mocks/NoopLogger';
import {NoopGatewayService} from '@fluxer/api/src/test/NoopGatewayService';
import {NoopWorkerService} from '@fluxer/api/src/test/NoopWorkerService';
import {TestMediaService} from '@fluxer/api/src/test/TestMediaService';
import {TestS3Service} from '@fluxer/api/src/test/TestS3Service';
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
import {DEFAULT_SEARCH_CLIENT_TIMEOUT_MS} from '@fluxer/constants/src/Timeouts';
import type {IKVProvider} from '@fluxer/kv_client/src/IKVProvider';

export interface ApiTestHarness {
	app: HonoApp;
	kvProvider: IKVProvider;
	mockBlueskyOAuthService: MockBlueskyOAuthService;
	reset: () => Promise<void>;
	resetData: () => Promise<void>;
	shutdown: () => Promise<void>;
	requestJson: (params: {
		path: string;
		method?: string;
		body?: unknown;
		headers?: Record<string, string>;
	}) => Promise<Response>;
}

export interface CreateApiTestHarnessOptions {
	search?: 'disabled' | 'meilisearch';
}

export async function createApiTestHarness(options: CreateApiTestHarnessOptions = {}): Promise<ApiTestHarness> {
	const kvProvider = new MockKVProvider();
	setInjectedKVProvider(kvProvider);
	setInjectedGatewayService(new NoopGatewayService());
	setInjectedWorkerService(new NoopWorkerService());

	const s3Service = new TestS3Service();
	await s3Service.initialize();
	setInjectedS3Service(s3Service);

	const mediaService = new TestMediaService();
	mediaService.setS3Service(s3Service);
	setInjectedMediaService(mediaService);

	const harnessLogger = new NoopLogger();
	let searchProvider: ISearchProvider | null = null;
	let releaseMeilisearch: (() => Promise<void>) | null = null;
	if (options.search === 'meilisearch') {
		const server = await acquireMeilisearchTestServer();
		releaseMeilisearch = server.release;
		searchProvider = new MeilisearchSearchProvider({
			config: {
				url: server.url,
				apiKey: server.apiKey,
				timeoutMs: DEFAULT_SEARCH_CLIENT_TIMEOUT_MS,
				taskWaitTimeoutMs: DEFAULT_SEARCH_CLIENT_TIMEOUT_MS,
				taskPollIntervalMs: 50,
			},
			logger: harnessLogger,
		});
		await searchProvider.initialize();
	} else {
		searchProvider = new NullSearchProvider();
		await searchProvider.initialize();
	}
	setInjectedSearchProviderService(searchProvider);

	const mockBlueskyOAuthService = new MockBlueskyOAuthService();
	setInjectedBlueskyOAuthService(mockBlueskyOAuthService);

	const {
		app,
		initialize: initializeApp,
		shutdown: shutdownApp,
	} = await createAPIApp({
		config: Config,
		logger: harnessLogger,
	});

	try {
		await initializeApp();
	} catch (error) {
		console.error('Failed to initialize API app for tests:', error);
		throw error;
	}

	async function reset(): Promise<void> {
		clearSqliteStore();
		kvProvider.reset();
		mockBlueskyOAuthService.reset();
	}

	async function resetData(): Promise<void> {
		kvProvider.reset();
	}

	async function shutdown(): Promise<void> {
		try {
			await shutdownApp();
		} catch (_error) {}
		if (searchProvider) {
			try {
				await searchProvider.shutdown();
			} catch (_error) {}
		}
		setInjectedWorkerService(undefined);
		setInjectedGatewayService(undefined);
		setInjectedKVProvider(undefined);
		setInjectedMediaService(undefined);
		setInjectedS3Service(undefined);
		setInjectedSearchProviderService(undefined);
		setInjectedBlueskyOAuthService(undefined);
		await s3Service.cleanup();
		if (releaseMeilisearch) {
			await releaseMeilisearch();
		}
	}

	async function requestJson(params: {
		path: string;
		method?: string;
		body?: unknown;
		headers?: Record<string, string>;
	}): Promise<Response> {
		const {path, body, method = 'GET', headers} = params;
		const mergedHeaders = new Headers(headers);
		if (!mergedHeaders.has('content-type')) {
			mergedHeaders.set('content-type', 'application/json');
		}
		if (!mergedHeaders.has('x-forwarded-for')) {
			mergedHeaders.set('x-forwarded-for', '127.0.0.1');
		}

		const contentType = mergedHeaders.get('content-type');
		let requestBody: string | undefined;
		if (body !== undefined) {
			if (typeof body === 'string') {
				requestBody = body;
			} else if (contentType === 'application/json') {
				requestBody = JSON.stringify(body);
			} else {
				requestBody = JSON.stringify(body);
			}
		}

		return app.request(path, {
			method,
			headers: mergedHeaders,
			body: requestBody,
		});
	}

	return {app, kvProvider, mockBlueskyOAuthService, reset, resetData, shutdown, requestJson};
}
