import "fake-indexeddb/auto";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { openDB } from "../storage/db";
import type { GeminiConfig } from "../api/config";

// Mock modules before importing the module under test
vi.mock("../api/config", () => ({
	loadConfig: vi.fn(),
	resetConfigCache: vi.fn(),
}));

vi.mock("../api/gemini", () => ({
	generateText: vi.fn(),
	checkRateLimit: vi.fn(),
	incrementApiCall: vi.fn(),
}));

import { loadConfig } from "../api/config";
import {
	checkRateLimit,
	generateText,
	incrementApiCall,
} from "../api/gemini";
import { getGreeting, getLetterIntro, getForestComment } from "./companion";

const TEST_CONFIG: GeminiConfig = {
	geminiApiKey: "test-key",
	geminiModel: "gemini-2.5-flash",
	imageModel: "gemini-3.1-flash-preview-image",
};

let testCounter = 0;
let db: IDBDatabase;

beforeEach(async () => {
	vi.clearAllMocks();
	testCounter++;
	db = await openDB(`companion-test-${testCounter}`);
});

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

describe("getGreeting", () => {
	it("returns AI text when API succeeds and rate limit not exceeded", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue(
			"Guten Morgen, kleiner Waldfreund!",
		);

		const result = await getGreeting("fee", db);

		expect(result).toBe("Guten Morgen, kleiner Waldfreund!");
		expect(generateText).toHaveBeenCalledTimes(1);
		expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
	});

	it("returns fallback text when loadConfig returns null (no API key)", async () => {
		vi.mocked(loadConfig).mockResolvedValue(null);

		const result = await getGreeting("fee", db);

		expect(result).toBeTruthy();
		expect(typeof result).toBe("string");
		expect(generateText).not.toHaveBeenCalled();
	});

	it("returns fallback text when generateText returns null (API error)", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue(null);

		const result = await getGreeting("fee", db);

		expect(result).toBeTruthy();
		expect(typeof result).toBe("string");
		expect(incrementApiCall).not.toHaveBeenCalled();
	});

	it("returns fallback text when rate limit exceeded", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(false);

		const result = await getGreeting("fee", db);

		expect(result).toBeTruthy();
		expect(generateText).not.toHaveBeenCalled();
	});

	it("passes correct system prompt with companion personality and time of day", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue("Hallo!");

		await getGreeting("fee", db);

		const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
		expect(systemPrompt).toContain("Lila");
		expect(systemPrompt).toContain(
			"Sanft, ermutigend, ein bisschen verträumt",
		);
		expect(systemPrompt).toMatch(/morgen|mittag|abend/);
		expect(systemPrompt).toContain("Max. 2 kurze Sätze");
	});

	it("calls incrementApiCall after successful API call", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue("Hallo Waldkind!");

		await getGreeting("einhorn", db);

		expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
	});
});

describe("getLetterIntro", () => {
	it("returns AI text when API succeeds", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue(
			"Das F ist wie ein Farn im Wald!",
		);

		const result = await getLetterIntro(
			"fee",
			"f",
			"linker Zeigefinger",
			db,
		);

		expect(result).toBe("Das F ist wie ein Farn im Wald!");
		expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
	});

	it("returns fallback LetterIntro text when API unavailable", async () => {
		vi.mocked(loadConfig).mockResolvedValue(null);

		const result = await getLetterIntro(
			"fee",
			"f",
			"linker Zeigefinger",
			db,
		);

		expect(result).toBeTruthy();
		expect(result).toContain("Zeigefinger");
		expect(generateText).not.toHaveBeenCalled();
	});

	it("system prompt includes letter, finger description, and companion personality", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue("Das D wohnt hier!");

		await getLetterIntro("fuchs", "d", "linker Mittelfinger", db);

		const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
		expect(systemPrompt).toContain("Finn");
		expect(systemPrompt).toContain("Ruhig, weise, humorvoll");
		expect(systemPrompt).toContain("d");
		expect(systemPrompt).toContain("linker Mittelfinger");
	});

	it("returns default text when fallback not found for unknown letter", async () => {
		vi.mocked(loadConfig).mockResolvedValue(null);

		const result = await getLetterIntro(
			"fee",
			"@",
			"linker kleiner Finger",
			db,
		);

		expect(result).toBe("Drücke die leuchtende Taste!");
	});
});

describe("getForestComment", () => {
	it("returns AI text when API succeeds", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue(
			"Oh, ein Schmetterling ist erschienen!",
		);

		const result = await getForestComment(
			"eule",
			"ein blauer Schmetterling",
			db,
		);

		expect(result).toBe("Oh, ein Schmetterling ist erschienen!");
		expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
	});

	it("returns fallback text when API unavailable", async () => {
		vi.mocked(loadConfig).mockResolvedValue(null);

		const result = await getForestComment(
			"eule",
			"ein blauer Schmetterling",
			db,
		);

		expect(result).toBeTruthy();
		expect(typeof result).toBe("string");
		expect(generateText).not.toHaveBeenCalled();
	});

	it("system prompt includes element description and companion personality", async () => {
		vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
		vi.mocked(checkRateLimit).mockResolvedValue(true);
		vi.mocked(generateText).mockResolvedValue("Wie wunderschön!");

		await getForestComment("einhorn", "ein leuchtender Pilz", db);

		const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
		expect(systemPrompt).toContain("Stella");
		expect(systemPrompt).toContain(
			"Fröhlich, enthusiastisch, feiert jeden kleinen Erfolg",
		);
		expect(systemPrompt).toContain("ein leuchtender Pilz");
	});
});
