import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProjectSettings } from "./settings";
import { settingsApi } from "./settings";

const mockFetch = vi.fn();

beforeEach(() => {
	vi.stubGlobal("fetch", mockFetch);
});

afterEach(() => {
	vi.restoreAllMocks();
});

function okResponse(body: unknown) {
	return new Response(JSON.stringify(body), {
		status: 200,
		headers: { "Content-Type": "application/json" },
	});
}

function errorResponse(status: number, text: string) {
	return new Response(text, { status });
}

const defaultProjectSettings: ProjectSettings = {
	default_qa: "server",
	default_coder_model: null,
	max_coders: null,
	max_retries: 2,
	base_branch: null,
	rate_limit_notifications: true,
	timezone: null,
	rendezvous: null,
	watcher_sweep_interval_secs: 60,
	watcher_done_retention_secs: 14400,
};

describe("settingsApi", () => {
	describe("getProjectSettings", () => {
		it("sends GET to /settings and returns project settings", async () => {
			mockFetch.mockResolvedValueOnce(okResponse(defaultProjectSettings));

			const result = await settingsApi.getProjectSettings();

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/settings",
				expect.objectContaining({
					headers: expect.objectContaining({
						"Content-Type": "application/json",
					}),
				}),
			);
			expect(result).toEqual(defaultProjectSettings);
		});

		it("uses custom baseUrl when provided", async () => {
			mockFetch.mockResolvedValueOnce(okResponse(defaultProjectSettings));
			await settingsApi.getProjectSettings("http://localhost:4000/api");
			expect(mockFetch).toHaveBeenCalledWith(
				"http://localhost:4000/api/settings",
				expect.anything(),
			);
		});
	});

	describe("putProjectSettings", () => {
		it("sends PUT to /settings with settings body", async () => {
			const updated = { ...defaultProjectSettings, default_qa: "agent" };
			mockFetch.mockResolvedValueOnce(okResponse(updated));

			const result = await settingsApi.putProjectSettings(updated);

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/settings",
				expect.objectContaining({
					method: "PUT",
					body: JSON.stringify(updated),
				}),
			);
			expect(result.default_qa).toBe("agent");
		});

		it("throws on validation error", async () => {
			mockFetch.mockResolvedValueOnce(
				errorResponse(400, "Invalid default_qa value"),
			);
			await expect(
				settingsApi.putProjectSettings({
					...defaultProjectSettings,
					default_qa: "invalid",
				}),
			).rejects.toThrow("Invalid default_qa value");
		});
	});

	describe("getEditorCommand", () => {
		it("sends GET to /settings/editor and returns editor settings", async () => {
			const expected = { editor_command: "zed" };
			mockFetch.mockResolvedValueOnce(okResponse(expected));

			const result = await settingsApi.getEditorCommand();

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/settings/editor",
				expect.objectContaining({
					headers: expect.objectContaining({
						"Content-Type": "application/json",
					}),
				}),
			);
			expect(result).toEqual(expected);
		});

		it("returns null editor_command when not configured", async () => {
			const expected = { editor_command: null };
			mockFetch.mockResolvedValueOnce(okResponse(expected));

			const result = await settingsApi.getEditorCommand();
			expect(result.editor_command).toBeNull();
		});

		it("uses custom baseUrl when provided", async () => {
			mockFetch.mockResolvedValueOnce(okResponse({ editor_command: "code" }));

			await settingsApi.getEditorCommand("http://localhost:4000/api");

			expect(mockFetch).toHaveBeenCalledWith(
				"http://localhost:4000/api/settings/editor",
				expect.anything(),
			);
		});
	});

	describe("setEditorCommand", () => {
		it("sends PUT to /settings/editor with command body", async () => {
			const expected = { editor_command: "zed" };
			mockFetch.mockResolvedValueOnce(okResponse(expected));

			const result = await settingsApi.setEditorCommand("zed");

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/settings/editor",
				expect.objectContaining({
					method: "PUT",
					body: JSON.stringify({ editor_command: "zed" }),
				}),
			);
			expect(result).toEqual(expected);
		});

		it("sends PUT with null to clear the editor command", async () => {
			const expected = { editor_command: null };
			mockFetch.mockResolvedValueOnce(okResponse(expected));

			const result = await settingsApi.setEditorCommand(null);

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/settings/editor",
				expect.objectContaining({
					method: "PUT",
					body: JSON.stringify({ editor_command: null }),
				}),
			);
			expect(result.editor_command).toBeNull();
		});

		it("uses custom baseUrl when provided", async () => {
			mockFetch.mockResolvedValueOnce(okResponse({ editor_command: "vim" }));

			await settingsApi.setEditorCommand("vim", "http://localhost:4000/api");

			expect(mockFetch).toHaveBeenCalledWith(
				"http://localhost:4000/api/settings/editor",
				expect.objectContaining({ method: "PUT" }),
			);
		});
	});

	describe("error handling", () => {
		it("throws with response body text on non-ok response", async () => {
			mockFetch.mockResolvedValueOnce(errorResponse(400, "Bad Request"));

			await expect(settingsApi.getEditorCommand()).rejects.toThrow(
				"Bad Request",
			);
		});

		it("throws with status code message when response body is empty", async () => {
			mockFetch.mockResolvedValueOnce(errorResponse(500, ""));

			await expect(settingsApi.getEditorCommand()).rejects.toThrow(
				"Request failed (500)",
			);
		});

		it("throws on setEditorCommand error", async () => {
			mockFetch.mockResolvedValueOnce(errorResponse(403, "Forbidden"));

			await expect(settingsApi.setEditorCommand("code")).rejects.toThrow(
				"Forbidden",
			);
		});
	});
});
