import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";

import { DIProvider } from "@/app/di";
import { MockModelServerGateway, MockProfileGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import type { LocalModelServerConfig, ProfileModelCatalog } from "@/domain";
import { ProfilesSettings } from "./ProfilesSettings";

function renderSettings(
  profile: MockProfileGateway = new MockProfileGateway(),
  modelServer: MockModelServerGateway = new MockModelServerGateway(),
) {
  return {
    profile,
    modelServer,
    ...render(
      <DIProvider gateways={{ profile, modelServer } as unknown as Gateways}>
        <ProfilesSettings />
      </DIProvider>,
    ),
  };
}

function chooseDropdownOption(label: string | RegExp, optionName: string | RegExp) {
  fireEvent.click(screen.getByLabelText(label));
  fireEvent.click(screen.getByRole("option", { name: optionName }));
}

async function waitReady() {
  await waitFor(() =>
    expect(
      (screen.getByRole("button", { name: "Creer un profil" }) as HTMLButtonElement)
        .disabled,
    ).toBe(false),
  );
}

async function createProfile() {
  const before = screen.queryAllByRole("listitem").length;
  fireEvent.click(screen.getByRole("button", { name: "Creer un profil" }));
  await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(before + 1));
}

describe("ProfilesSettings", () => {
  it("creates multiple named Codex and Claude profiles with different models", async () => {
    const { profile } = renderSettings();
    await waitReady();

    await createProfile();
    await createProfile();
    let rows = screen.getAllByRole("listitem");
    fireEvent.change(within(rows[1]).getByLabelText(/nom du profil/), {
      target: { value: "Codex mini" },
    });
    fireEvent.change(within(rows[1]).getByLabelText(/modele du profil/), {
      target: { value: "gpt-5-mini" },
    });
    fireEvent.click(within(rows[1]).getByRole("button", { name: "Enregistrer" }));

    fireEvent.click(screen.getByRole("tab", { name: "Claude" }));
    await waitReady();
    await createProfile();
    await createProfile();
    rows = screen.getAllByRole("listitem");
    fireEvent.change(within(rows[1]).getByLabelText(/nom du profil/), {
      target: { value: "Claude Opus" },
    });
    fireEvent.change(within(rows[1]).getByLabelText(/modele du profil/), {
      target: { value: "claude-opus-4-8" },
    });
    fireEvent.click(within(rows[1]).getByRole("button", { name: "Enregistrer" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved.filter((p) => p.structuredAdapter === "codex")).toHaveLength(2);
      expect(saved.filter((p) => p.structuredAdapter === "claude")).toHaveLength(2);
      expect(saved.map((p) => p.model)).toEqual(
        expect.arrayContaining([
          "gpt-5-codex",
          "gpt-5-mini",
          "claude-sonnet-5",
          "claude-opus-4-8",
        ]),
      );
    });
  });

  it("duplicates from an existing profile with '<name> copy' and preserves the model", async () => {
    const { profile } = renderSettings();
    await waitReady();
    await createProfile();

    const row = screen.getAllByRole("listitem")[0];
    fireEvent.click(within(row).getByRole("button", { name: "Dupliquer" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved.some((p) => p.name === "OpenAI Codex CLI copy copy")).toBe(true);
      expect(saved.filter((p) => p.model === "gpt-5-codex")).toHaveLength(2);
    });
  });

  it("keeps manual model entry available when the catalogue fails", async () => {
    class CatalogueDownProfileGateway extends MockProfileGateway {
      listCodexModels(): Promise<ProfileModelCatalog> {
        return Promise.reject(new Error("catalogue down"));
      }
    }

    renderSettings(new CatalogueDownProfileGateway());
    await waitReady();
    expect(await screen.findByText(/Catalogue provider indisponible/)).toBeTruthy();

    await createProfile();
    const model = within(screen.getAllByRole("listitem")[0]).getByLabelText(
      /modele du profil/,
    ) as HTMLInputElement;
    fireEvent.change(model, { target: { value: "future-codex-model" } });
    expect(model.value).toBe("future-codex-model");
    expect(screen.getAllByText(/Catalogue provider indisponible/).length).toBeGreaterThan(0);
  });

  it("shows compatibility states in suggestions and contextual help", async () => {
    renderSettings();
    await waitReady();
    await createProfile();

    const row = screen.getAllByRole("listitem")[0];
    const model = within(row).getByLabelText(/modele du profil/) as HTMLInputElement;
    fireEvent.focus(model);

    expect(await within(row).findByText("Compatible")).toBeTruthy();
    expect(
      within(row).getByText(
        /Compatible avec Codex CLI 0\.145\.0 d'après le catalogue local IdeA\./,
      ),
    ).toBeTruthy();

    fireEvent.change(model, { target: { value: "" } });
    expect(within(row).getAllByText("Inconnu").length).toBeGreaterThan(0);
    expect(within(row).getByText("Probablement trop récent")).toBeTruthy();

    fireEvent.change(model, { target: { value: "future-codex-model" } });
    expect(within(row).getByText("Inconnu")).toBeTruthy();
    expect(
      within(row).getByText(
        /Compatibilité non connue pour Codex CLI 0\.145\.0 ; la saisie reste autorisée\./,
      ),
    ).toBeTruthy();
  });

  it("saves likely-too-recent models and shows a non-blocking warning", async () => {
    const { profile } = renderSettings();
    await waitReady();
    await createProfile();

    const row = screen.getAllByRole("listitem")[0];
    const model = within(row).getByLabelText(/modele du profil/) as HTMLInputElement;
    fireEvent.change(model, { target: { value: "gpt-5-mini" } });
    fireEvent.click(within(row).getByRole("button", { name: "Enregistrer" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved.some((p) => p.model === "gpt-5-mini")).toBe(true);
    });
    expect(
      await screen.findByText(/Ce modèle semble plus récent que votre Codex CLI 0\.145\.0/),
    ).toBeTruthy();
  });

  it("restores OpenCode local servers, local binding, and cloud provider choice in settings", async () => {
    const modelServer = new MockModelServerGateway();
    const server: LocalModelServerConfig = {
      id: "550e8400-e29b-41d4-a716-446655440000",
      kind: "llamaCpp",
      name: "Local A",
      baseURL: "http://localhost:8080/v1",
      port: 8080,
      servedModelName: "qwen3-coder-30b",
      host: "127.0.0.1",
      jinja: false,
      args: [],
      autoStart: false,
      stopPolicy: "stopOnAppExit",
    };
    await modelServer.saveModelServer(server);

    const { profile } = renderSettings(new MockProfileGateway(), modelServer);
    await waitReady();

    fireEvent.click(screen.getByRole("tab", { name: "OpenCode-local" }));
    await waitReady();

    expect(screen.getByRole("button", { name: "add model server" })).toBeTruthy();
    expect(screen.getByText("Local A")).toBeTruthy();

    await createProfile();
    await screen.findByLabelText(
      "OpenCode + llama.cpp copy local model server",
    );
    chooseDropdownOption(
      "OpenCode + llama.cpp copy local model server",
      /Local A/,
    );

    const profileRow = screen
      .getByLabelText("OpenCode + llama.cpp copy local model server")
      .closest("li");
    expect(profileRow).not.toBeNull();
    fireEvent.click(
      within(profileRow as HTMLElement).getByRole("button", {
        name: "Enregistrer",
      }),
    );

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      const opencode = saved.find((p) => p.structuredAdapter === "openCode");
      expect(opencode?.opencode?.localModelServerId).toBe(server.id);
    });

    fireEvent.click(
      within(profileRow as HTMLElement).getByRole("radio", {
        name: "Provider cloud",
      }),
    );
    expect(
      await within(profileRow as HTMLElement).findByLabelText(
        "OpenCode + llama.cpp copy provider",
      ),
    ).toBeTruthy();
  });

  it("creates OpenCode profiles through the OpenCode clone flow and persists them for agent pickers", async () => {
    const profile = new MockProfileGateway();
    const cloneOpenCodeSpy = vi.spyOn(profile, "cloneOpenCodeProfileFromSeed");
    const cloneGenericSpy = vi.spyOn(profile, "cloneProfileFromSeed");
    renderSettings(profile);
    await waitReady();

    fireEvent.click(screen.getByRole("tab", { name: "OpenCode-local" }));
    await waitReady();

    fireEvent.click(screen.getByRole("button", { name: "Creer un profil" }));

    const name = await screen.findByDisplayValue("OpenCode + llama.cpp copy");
    expect(name).toBeTruthy();
    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved.map((p) => p.name)).toContain("OpenCode + llama.cpp copy");
      expect(saved.some((p) => p.structuredAdapter === "openCode")).toBe(true);
    });
    expect(cloneOpenCodeSpy).toHaveBeenCalledWith({
      name: "OpenCode + llama.cpp copy",
      opencode: expect.objectContaining({
        baseURL: "http://localhost:8080/v1",
        model: "qwen3-coder-30b",
      }),
    });
    expect(cloneGenericSpy).not.toHaveBeenCalled();
  });

  it("duplicates OpenCode profiles through the OpenCode clone flow and preserves their local config", async () => {
    const profile = new MockProfileGateway();
    renderSettings(profile);
    await waitReady();

    fireEvent.click(screen.getByRole("tab", { name: "OpenCode-local" }));
    await waitReady();
    await createProfile();

    const row = screen.getAllByRole("listitem")[0];
    fireEvent.change(within(row).getByLabelText("OpenCode + llama.cpp copy base url"), {
      target: { value: "http://localhost:9090/v1" },
    });
    fireEvent.change(within(row).getByLabelText("OpenCode + llama.cpp copy model"), {
      target: { value: "local-special" },
    });
    fireEvent.click(within(row).getByRole("button", { name: "Enregistrer" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved[0]?.opencode?.baseURL).toBe("http://localhost:9090/v1");
    });

    fireEvent.click(within(screen.getAllByRole("listitem")[0]).getByRole("button", {
      name: "Dupliquer",
    }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      const copies = saved.filter((p) => p.structuredAdapter === "openCode");
      expect(copies).toHaveLength(2);
      expect(copies[1]?.name).toBe("OpenCode + llama.cpp copy copy");
      expect(copies[1]?.opencode?.baseURL).toBe("http://localhost:9090/v1");
      expect(copies[1]?.opencode?.model).toBe("local-special");
    });
  });
});
