import { afterEach, describe, expect, mock, test } from "bun:test";
import type { AxiosAdapter } from "axios";
import { StrictMode } from "react";
import { MemoryRouter, Route, Routes } from "react-router";
import TestRenderer, { act } from "react-test-renderer";

import InstallApp from "./install-app";
import { generatedAxios } from "./lib/api-client";

const INSTALL_ERROR_MESSAGE =
  "GitHub App setup failed before Fabro could save the app credentials. Continue again to retry the callback.";
const INSTALL_PREFILL = {
  canonical_url: "https://fabro.example.com",
  object_store_local_root: "/home/test/.fabro/storage/objects",
};

const SESSION_RESPONSE = {
  completed_steps: ["server", "object_store", "llm"],
  llm: null,
  server: { canonical_url: "https://fabro.example.com" },
  object_store: { provider: "local" },
  github: null,
  prefill: INSTALL_PREFILL,
};

const originalAdapter = generatedAxios.defaults.adapter;

type TestWindow = {
  history: {
    state: unknown;
    replaceState: (state: unknown, unused: string, url?: string | URL | null) => void;
  };
  location: {
    href: string;
    pathname: string;
    search: string;
  };
  sessionStorage: {
    clear: () => void;
    getItem: (key: string) => string | null;
    removeItem: (key: string) => void;
    setItem: (key: string, value: string) => void;
  };
  setInterval: typeof setInterval;
  clearInterval: typeof clearInterval;
  setTimeout: typeof setTimeout;
  clearTimeout: typeof clearTimeout;
};

function createTestWindow(initialUrl: string): TestWindow {
  let current = new URL(initialUrl);
  const sessionStorage = new Map<string, string>();

  const location = {
    get href() {
      return current.toString();
    },
    set href(value: string) {
      current = new URL(value, current.origin);
    },
    get pathname() {
      return current.pathname;
    },
    get search() {
      return current.search;
    },
  };

  return {
    history: {
      state: null,
      replaceState(state, _unused, url) {
        this.state = state;
        if (url) {
          current = new URL(String(url), current.origin);
        }
      },
    },
    location,
    sessionStorage: {
      clear() {
        sessionStorage.clear();
      },
      getItem(key) {
        return sessionStorage.get(key) ?? null;
      },
      removeItem(key) {
        sessionStorage.delete(key);
      },
      setItem(key, value) {
        sessionStorage.set(key, value);
      },
    },
    setInterval,
    clearInterval,
    setTimeout,
    clearTimeout,
  };
}

function renderTreeText(
  node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>,
): string {
  if (!node) return "";
  if (typeof node === "string") return node;
  if (Array.isArray(node)) return node.map(renderTreeText).join("");
  return (node.children ?? []).map(renderTreeText).join("");
}

function findOptionButton(
  renderer: TestRenderer.ReactTestRenderer,
  title: string,
): TestRenderer.ReactTestInstance {
  const button = renderer.root.findAll((node) => {
    if (node.type !== "button" || node.props["aria-pressed"] === undefined) {
      return false;
    }
    const titleSpan = node.findAll(
      (child) => child.type === "span" && child.children[0] === title,
    );
    return titleSpan.length > 0;
  })[0];
  if (!button) {
    throw new Error(`option button "${title}" not found`);
  }
  return button;
}

function useInstallFetchMock(fetchMock: typeof fetch) {
  generatedAxios.defaults.adapter = (async (config) => {
    const response = await fetchMock(config.url ?? "", {
      method: config.method?.toUpperCase(),
      headers: config.headers as HeadersInit,
      body: config.data as BodyInit | undefined,
      signal: config.signal,
    });
    const text = response.status === 204 ? "" : await response.text();
    const data = text ? JSON.parse(text) : undefined;
    const axiosResponse = {
      data,
      status: response.status,
      statusText: response.statusText,
      headers: Object.fromEntries(response.headers.entries()),
      config,
    };
    if (response.status >= 400) {
      throw {
        isAxiosError: true,
        message: response.statusText || `HTTP ${response.status}`,
        response: axiosResponse,
      };
    }
    return axiosResponse;
  }) as AxiosAdapter;
}

async function waitFor(assertion: () => void, timeoutMs = 1000): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  let lastError: unknown;
  while (Date.now() < deadline) {
    try {
      assertion();
      return;
    } catch (error) {
      lastError = error;
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
  }
  throw lastError;
}

describe("InstallApp", () => {
  afterEach(() => {
    generatedAxios.defaults.adapter = originalAdapter;
    delete (globalThis as { window?: unknown }).window;
    delete (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT;
    mock.restore();
  });

  test("renders the GitHub callback error on the GitHub install step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(JSON.stringify(SESSION_RESPONSE), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow(
        "https://fabro.example.com/install/github?error=github-app-manifest-conversion-failed",
      );
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <StrictMode>
            <MemoryRouter initialEntries={["/install/github?error=github-app-manifest-conversion-failed"]}>
              <Routes>
                <Route path="/install/*" element={<InstallApp />} />
              </Routes>
            </MemoryRouter>
          </StrictMode>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain(INSTALL_ERROR_MESSAGE);
      });
      expect(testWindow.location.search).toBe("");

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("renders the GitHub App done screen after the manifest callback resolves", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      // Simulate the server-side /install/github/app/redirect handler having
      // just run — session returns a fully-populated `github.app` payload.
      const sessionResponse = {
        completed_steps: ["server", "object_store", "llm", "github"],
        llm: null,
        server: { canonical_url: "https://fabro.example.com" },
        object_store: { provider: "local" },
        github: {
          strategy: "app",
          owner: { kind: "personal" },
          app_name: "Fabro",
          slug: "fabro-brynary",
          allowed_username: "brynary",
        },
        prefill: INSTALL_PREFILL,
      };
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(JSON.stringify(sessionResponse), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow(
        "https://fabro.example.com/install/github/done?token=test-install-token",
      );
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/github/done?token=test-install-token"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("GitHub App connected");
        expect(text).toContain("fabro-brynary");
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("saves local disk object-store settings and advances to the sandbox step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
      const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
        fetchCalls.push({ input, init });
        if (String(input) === "/install/session" && fetchCalls.length === 1) {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: null,
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );
        }
        if (String(input) === "/install/object-store") {
          return Promise.resolve(new Response(null, { status: 204 }));
        }
        if (String(input) === "/install/session" && fetchCalls.length === 3) {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/object-store");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/object-store"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain(
          "Choose the shared object store",
        );
      });

      const form = renderer!.root.findByType("form");
      await act(async () => {
        form.props.onSubmit({ preventDefault() {} });
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
      });
      const backLink = renderer!.root.findAll(
        (node) =>
          node.type === "a" &&
          node.props.href === "/install/object-store" &&
          node.children.includes("Back"),
      );
      expect(backLink).toHaveLength(1);
      expect(fetchCalls.map((call) => String(call.input))).toEqual([
        "/install/session",
        "/install/object-store",
        "/install/session",
      ]);
      expect(fetchCalls[1]?.init?.body).toBe(
        JSON.stringify({
          provider: "local",
          root: INSTALL_PREFILL.object_store_local_root,
        }),
      );

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("rehydrates saved manual S3 credentials without exposing the secrets", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store"],
              llm: null,
              server: { canonical_url: "https://fabro.example.com" },
              object_store: {
                provider: "s3",
                bucket: "fabro-data",
                region: "us-east-1",
                credential_mode: "access_key",
                manual_credentials_saved: true,
              },
              github: null,
              prefill: INSTALL_PREFILL,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/object-store");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/object-store"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain(
          "Credentials saved. Leave both fields blank to keep them, or enter both fields to replace them.",
        );
      });

      expect(renderer!.root.findByProps({ name: "aws_access_key_id" }).props.value).toBe("");
      expect(renderer!.root.findByProps({ name: "aws_secret_access_key" }).props.value).toBe("");
      expect(renderTreeText(renderer!.toJSON())).not.toContain("AKIA");

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("shows the redacted object-store summary on the review step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store", "llm", "github"],
              llm: {
                providers: [{ provider: "anthropic" }],
              },
              server: { canonical_url: "https://fabro.example.com" },
              object_store: {
                provider: "s3",
                bucket: "fabro-data",
                region: "us-east-1",
                credential_mode: "access_key",
                manual_credentials_saved: true,
              },
              github: { strategy: "token", username: "octocat" },
              prefill: INSTALL_PREFILL,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/review");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/review"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("AWS S3");
        expect(text).toContain("fabro-data");
        expect(text).toContain("us-east-1");
        expect(text).toContain("Access key");
        expect(text).toContain("slatedb/, artifacts/");
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("shows the sandbox provider on the review step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock(() =>
        Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store", "sandbox", "llm", "github"],
              llm: { providers: [{ provider: "anthropic" }] },
              server: { canonical_url: "https://fabro.example.com" },
              object_store: { provider: "local" },
              sandbox: { provider: "daytona", api_key_saved: true },
              github: { strategy: "token", username: "octocat" },
              prefill: INSTALL_PREFILL,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        ),
      );
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/review");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/review"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("Daytona");
        expect(text).toContain("Saved");
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("validates Daytona key, saves sandbox, and advances to the LLM step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
      const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
        fetchCalls.push({ input, init });
        if (String(input) === "/install/session" && fetchCalls.length === 1) {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: null,
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );
        }
        if (
          String(input) === "/install/sandbox/test"
          || String(input) === "/install/sandbox"
        ) {
          return Promise.resolve(
            new Response(JSON.stringify({ ok: true }), {
              status: String(input).endsWith("/test") ? 200 : 204,
            }),
          );
        }
        if (String(input) === "/install/session") {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store", "sandbox"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: { provider: "daytona", api_key_saved: true },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/sandbox");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/sandbox"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
      });

      const daytonaButton = findOptionButton(renderer!, "Daytona");
      await act(async () => {
        daytonaButton.props.onClick();
      });

      const apiKeyInput = renderer!.root.findByProps({ name: "sandbox_api_key" });
      await act(async () => {
        apiKeyInput.props.onChange({ target: { value: "dtn_secret" } });
      });

      const form = renderer!.root.findByType("form");
      await act(async () => {
        form.props.onSubmit({ preventDefault() {} });
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
      });
      const calls = fetchCalls.map((call) => String(call.input));
      const testIdx = calls.indexOf("/install/sandbox/test");
      const putIdx = calls.indexOf("/install/sandbox");
      expect(testIdx).toBeGreaterThanOrEqual(0);
      expect(putIdx).toBeGreaterThan(testIdx);
      const sandboxTestCall = fetchCalls[testIdx];
      expect(sandboxTestCall?.init?.body).toBe(
        JSON.stringify({ provider: "daytona", allow_local: true, api_key: "dtn_secret" }),
      );

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("blocks Daytona save when the API key is missing", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchCalls: Array<{ input: RequestInfo | URL }> = [];
      const fetchMock = mock((input: RequestInfo | URL) => {
        fetchCalls.push({ input });
        if (String(input) === "/install/session") {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: null,
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/sandbox");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/sandbox"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
      });

      const daytonaButton = findOptionButton(renderer!, "Daytona");
      await act(async () => {
        daytonaButton.props.onClick();
      });

      const form = renderer!.root.findByType("form");
      await act(async () => {
        form.props.onSubmit({ preventDefault() {} });
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain(
          "Enter the Daytona API key before continuing.",
        );
      });
      expect(fetchCalls.map((call) => String(call.input))).toEqual([
        "/install/session",
      ]);

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("shows the GitHub App callback URL on the review step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store", "llm", "github"],
              llm: {
                providers: [{ provider: "anthropic" }],
              },
              server: { canonical_url: "https://fabro.example.com" },
              object_store: { provider: "local" },
              github: {
                strategy: "app",
                owner: { kind: "personal" },
                app_name: "octocat-fabro",
                slug: "octocat-fabro",
                allowed_username: "octocat",
              },
              prefill: INSTALL_PREFILL,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/review");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/review"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("GitHub callback URL");
        expect(text).toContain("https://fabro.example.com/auth/callback/github");
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("skips LLM setup with an empty providers list and advances to GitHub", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
      const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
        fetchCalls.push({ input, init });
        if (String(input) === "/install/session" && fetchCalls.length === 1) {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store", "sandbox"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: { provider: "docker" },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              { status: 200, headers: { "Content-Type": "application/json" } },
            ),
          );
        }
        if (String(input) === "/install/llm") {
          return Promise.resolve(new Response(null, { status: 204 }));
        }
        if (String(input) === "/install/session") {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store", "sandbox", "llm"],
                llm: { providers: [] },
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: { provider: "docker" },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              { status: 200, headers: { "Content-Type": "application/json" } },
            ),
          );
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/llm");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/llm"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
      });

      const skipButton = renderer!.root.findAll(
        (node) => node.type === "button" && node.children.includes("Skip LLM setup"),
      )[0];
      expect(skipButton).toBeDefined();
      await act(async () => {
        skipButton!.props.onClick();
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Connect GitHub");
      });

      const calls = fetchCalls.map((call) => String(call.input));
      const putIdx = calls.indexOf("/install/llm");
      expect(putIdx).toBeGreaterThanOrEqual(0);
      expect(fetchCalls[putIdx]?.init?.body).toBe(JSON.stringify({ providers: [] }));

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("blocks Continue on the LLM step when no API keys are entered", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        if (String(input) === "/install/session") {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store", "sandbox"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: { provider: "docker" },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              { status: 200, headers: { "Content-Type": "application/json" } },
            ),
          );
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/llm");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/llm"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
      });

      const form = renderer!.root.findByType("form");
      await act(async () => {
        form.props.onSubmit({ preventDefault() {} });
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain(
          "Add at least one provider API key before continuing.",
        );
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("shows a skipped LLM step as Skipped on the review step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store", "sandbox", "llm", "github"],
              llm: { providers: [] },
              server: { canonical_url: "https://fabro.example.com" },
              object_store: { provider: "local" },
              sandbox: { provider: "docker" },
              github: { strategy: "token", username: "octocat" },
              prefill: INSTALL_PREFILL,
            }),
            { status: 200, headers: { "Content-Type": "application/json" } },
          ),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/review");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/review"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("LLM providers");
        expect(text).toContain("Skipped");
      });

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("shows an incomplete LLM step as Not configured on the review step", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchMock = mock((input: RequestInfo | URL) => {
        expect(String(input)).toBe("/install/session");
        return Promise.resolve(
          new Response(
            JSON.stringify({
              completed_steps: ["server", "object_store", "sandbox", "github"],
              llm: null,
              server: { canonical_url: "https://fabro.example.com" },
              object_store: { provider: "local" },
              sandbox: { provider: "docker" },
              github: { strategy: "token", username: "octocat" },
              prefill: INSTALL_PREFILL,
            }),
            { status: 200, headers: { "Content-Type": "application/json" } },
          ),
        );
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/review");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/review"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        const text = renderTreeText(renderer!.toJSON());
        expect(text).toContain("LLM providers");
        expect(text).toContain("Not configured");
      });
      expect(renderTreeText(renderer!.toJSON())).not.toContain("Skipped");

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });

  test("keeps the user on the LLM step when skipping fails", async () => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
    const originalConsoleError = console.error;
    console.error = ((...args: unknown[]) => {
      if (
        typeof args[0] === "string" &&
        args[0].startsWith("react-test-renderer is deprecated")
      ) {
        return;
      }
      originalConsoleError(...args);
    }) as typeof console.error;
    try {
      const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
      const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
        fetchCalls.push({ input, init });
        if (String(input) === "/install/session") {
          return Promise.resolve(
            new Response(
              JSON.stringify({
                completed_steps: ["server", "object_store", "sandbox"],
                llm: null,
                server: { canonical_url: "https://fabro.example.com" },
                object_store: { provider: "local" },
                sandbox: { provider: "docker" },
                github: null,
                prefill: INSTALL_PREFILL,
              }),
              { status: 200, headers: { "Content-Type": "application/json" } },
            ),
          );
        }
        if (String(input) === "/install/llm") {
          return Promise.resolve(new Response(null, { status: 500 }));
        }
        throw new Error(`unexpected fetch: ${String(input)}`);
      });
      useInstallFetchMock(fetchMock as typeof fetch);

      const testWindow = createTestWindow("https://fabro.example.com/install/llm");
      testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
      (globalThis as { window?: unknown }).window = testWindow;

      let renderer: TestRenderer.ReactTestRenderer | null = null;
      await act(async () => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/install/llm"]}>
            <Routes>
              <Route path="/install/*" element={<InstallApp />} />
            </Routes>
          </MemoryRouter>,
        );
      });

      await waitFor(() => {
        expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
      });

      const skipButton = renderer!.root.findAll(
        (node) => node.type === "button" && node.children.includes("Skip LLM setup"),
      )[0];
      await act(async () => {
        skipButton!.props.onClick();
      });

      // The failed PUT must not advance to GitHub or refresh the session.
      await waitFor(() => {
        expect(fetchCalls.map((call) => String(call.input))).toContain("/install/llm");
      });
      const text = renderTreeText(renderer!.toJSON());
      expect(text).toContain("Add your LLM credentials");
      expect(text).not.toContain("Connect GitHub");
      expect(
        fetchCalls.filter((call) => String(call.input) === "/install/session"),
      ).toHaveLength(1);

      await act(async () => {
        renderer?.unmount();
      });
    } finally {
      console.error = originalConsoleError;
    }
  });
});
