import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { runInNewContext } from "node:vm";

import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";

import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { ThreadComposer } from "@/components/thread/ThreadComposer";
import { resources } from "@/i18n";
import {
  LOCALE_STORAGE_KEY,
  resolveInitialLocale,
  supportedLocales,
} from "@/i18n/config";

const QUICK_ACTION_KEYS = ["plan", "analyze", "brainstorm", "code", "summarize", "more"];
const IMAGE_QUICK_ACTION_KEYS = ["icon", "sticker", "poster", "product", "portrait", "edit"];
const HERO_GREETING_KEYS = ["workOn", "start", "build", "tackle"];
const SLASH_COMMAND_KEYS = [
  "new",
  "stop",
  "restart",
  "status",
  "model",
  "history",
  "dream",
  "dream_log",
  "dream_restore",
  "dream_prompt",
  "goal",
  "trigger",
  "help",
  "pairing",
];
const SETTINGS_NAV_KEYS = [
  "overview",
  "appearance",
  "models",
  "image",
  "browser",
  "apps",
  "automations",
  "runtime",
  "advanced",
];
const LOCALIZED_SETTINGS_COPY_KEYS = [
  "settings.backToChat",
  "settings.sidebar.title",
  "settings.sidebar.ariaLabel",
  "settings.nav.overview",
  "settings.nav.appearance",
  "settings.nav.models",
  "settings.nav.providers",
  "settings.nav.apps",
  "settings.nav.automations",
  "settings.nav.runtime",
  "settings.nav.advanced",
  "sidebar.automations",
  "settings.automations.filters.active",
  "settings.automations.queue",
  "settings.automations.empty",
  "settings.automations.systemTask",
  "settings.automations.labels.schedule",
  "settings.automations.status.active",
  "settings.automations.deleteTitle",
  "settings.sections.interface",
  "settings.sections.localPreferences",
  "settings.sections.webSearch",
  "settings.sections.webBehavior",
  "settings.sections.webuiSafety",
  "settings.sections.capabilities",
  "settings.sections.apps",
  "settings.apps.description",
  "settings.apps.caption",
  "settings.apps.restartRequired",
  "settings.mcp.connectingAccount",
  "settings.mcp.continueSignIn",
  "settings.mcp.preparingSignIn",
  "settings.mcp.openSignInToContinue",
  "settings.mcp.finishSignInInBrowser",
  "settings.mcp.finishingConnection",
  "settings.mcp.activatingTools",
  "settings.mcp.connected",
  "settings.mcp.connectionFailed",
  "settings.mcp.connectionCancelled",
  "settings.mcp.reloadFailed",
  "settings.mcp.oauthFailed",
  "settings.skills.views",
  "settings.skills.installedTab",
  "settings.skills.discoverTab",
  "settings.skills.customGroup",
  "settings.skills.builtinGroup",
  "settings.skills.otherGroup",
  "settings.skills.searchInstalled",
  "settings.skills.filterAll",
  "settings.skills.filterEnabled",
  "settings.skills.filterDisabled",
  "settings.skills.noMatching",
  "settings.skills.statusDisabled",
  "settings.skills.statusEnabled",
  "settings.skills.statusNeedsSetup",
  "settings.skills.showLess",
  "settings.skills.showMore",
  "settings.skills.enabledControl",
  "settings.skills.enabledDescription",
  "settings.skills.enableSkill",
  "settings.skills.disableSkill",
  "settings.skills.updateFailed",
  "settings.skills.deleteTitle",
  "settings.skills.deleteDescription",
  "settings.skills.deleteAction",
  "settings.skills.deleteFailed",
  "settings.skills.deleteConfirmTitle",
  "settings.skills.deleteConfirmDescription",
  "settings.skills.deleteConfirmAction",
  "settings.skills.instructionsTitle",
  "settings.skills.setupRequired",
  "settings.skills.setupDescription",
  "settings.skills.copySetupCommand",
  "settings.skills.checkAgain",
  "settings.skills.marketplaceSearchFailed",
  "settings.skills.marketplaceInstallFailed",
  "settings.skills.marketplaceSearchPlaceholder",
  "settings.skills.marketplaceSearchLabel",
  "settings.skills.marketplaceSearching",
  "settings.skills.marketplaceProviderFilter",
  "settings.skills.marketplaceProviderAll",
  "settings.skills.marketplaceTrendingTitle",
  "settings.skills.marketplaceTrendingDescription",
  "settings.skills.marketplaceViewAll",
  "settings.skills.marketplaceTrendingUnavailable",
  "settings.skills.marketplaceEmpty",
  "settings.skills.marketplaceConfirmTitle",
  "settings.skills.marketplaceConfirmDescription",
  "settings.skills.marketplaceConfirmInstall",
  "settings.skills.marketplaceOpen",
  "settings.skills.marketplaceOpenProvider",
  "settings.skills.marketplaceInstalls24h",
  "settings.skills.marketplaceInstalls",
  "settings.skills.marketplaceNpxRequired",
  "settings.skills.marketplaceInstalling",
  "settings.skills.marketplaceInstalled",
  "settings.skills.marketplaceInstall",
  "settings.skills.marketplaceNoTrend",
  "settings.skills.marketplaceTrendLabel",
  "settings.nanobotFeatures.disable",
  "settings.nanobotFeatures.ready",
  "settings.nanobotFeatures.missingDependency",
  "settings.nanobotFeatures.installConfirmTitle",
  "settings.nanobotFeatures.installConfirmDescription",
  "settings.nanobotFeatures.installConfirmAction",
  "settings.nanobotFeatures.channelDisabled",
  "settings.nanobotFeatures.notEnabled",
  "settings.sections.about",
  "settings.rows.theme",
  "settings.rows.language",
  "settings.rows.density",
  "settings.rows.activityMode",
  "settings.rows.fileEditDisplay",
  "settings.rows.codeWrap",
  "settings.rows.brandLogos",
  "settings.rows.browserNotifications",
  "settings.rows.currentModel",
  "settings.rows.localServiceAccess",
  "settings.rows.webuiDefaultAccess",
  "settings.rows.contextWindow",
  "settings.help.theme",
  "settings.help.language",
  "settings.help.density",
  "settings.help.activityMode",
  "settings.help.fileEditDisplay",
  "settings.help.codeWrap",
  "settings.help.brandLogos",
  "settings.help.browserNotifications",
  "settings.help.currentModel",
  "settings.help.localServiceAccess",
  "settings.help.webuiDefaultAccess",
  "settings.values.light",
  "settings.values.dark",
  "settings.values.comfortable",
  "settings.values.compact",
  "settings.values.expanded",
  "settings.values.enabled",
  "settings.values.disabled",
  "settings.values.defaultPermission",
  "settings.values.fullAccess",
  "settings.values.configured",
  "settings.values.notConfigured",
  "settings.status.loading",
  "settings.status.unsaved",
  "settings.status.upToDate",
  "settings.actions.save",
  "settings.actions.saving",
  "settings.about.checking",
  "settings.about.checkForUpdates",
  "settings.about.upToDate",
  "settings.about.updateAvailable",
];
const LOCALIZED_WORKSPACE_COPY_KEYS = [
  "thread.composer.workspace.accessAria",
  "thread.composer.workspace.default",
  "thread.composer.workspace.full",
  "errors.workspaceScopeRejected.title",
  "errors.workspaceScopeRejected.body",
  "workspace.dialog.defaultProject",
  "workspace.dialog.usePath",
  "workspace.dialog.absolutePathRequired",
];
const LOCALIZED_CHANNEL_SHELL_KEYS = [
  "settings.channels.advanced",
  "settings.channels.checkAndEnable",
  "settings.channels.checkConnection",
  "settings.channels.checkedAndEnabled",
  "settings.channels.checking",
  "settings.channels.checkOnly",
  "settings.channels.commandCopied",
  "settings.channels.commandCopyFailed",
  "settings.channels.configuredInstances",
  "settings.channels.connectPreview",
  "settings.channels.copyCommand",
  "settings.channels.filterAll",
  "settings.channels.filterOff",
  "settings.channels.filterOn",
  "settings.channels.helperCopied",
  "settings.channels.helperCopyFailed",
  "settings.channels.hideSecret",
  "settings.channels.instanceConfigured",
  "settings.channels.instanceNeedsSetup",
  "settings.channels.managedByWebui",
  "settings.channels.officialGuide",
  "settings.channels.optional",
  "settings.channels.providerPreset",
  "settings.channels.requiredSetup",
  "settings.channels.savedSecret",
  "settings.channels.savedSecretPlaceholder",
  "settings.channels.savedSettings",
  "settings.channels.saveSettings",
  "settings.channels.selectChannel",
  "settings.channels.setupSteps",
  "settings.channels.showSecret",
  "settings.channels.toggleChannel",
  "settings.channels.toggleInstance",
  "settings.channels.tryIt",
  "settings.channels.validation.connected",
  "settings.channels.validation.configured",
  "settings.channels.validation.invalid",
  "settings.channels.validation.needs_setup",
  "settings.channels.validation.unsupported",
  "settings.channels.validationFailed",
];
const LOCALIZED_NEW_SURFACE_KEYS = [
  "chat.activity.running",
  "chat.activity.complete",
  "chat.activity.updated",
  "chat.activity.recovery",
  "chat.pin",
  "chat.unpin",
  "chat.rename",
  "chat.renameProjectTitle",
  "chat.renameProjectDescription",
  "chat.renameProjectPlaceholder",
  "chat.renameSave",
  "chat.archive",
  "chat.unarchive",
  "chat.showArchived",
  "chat.hideArchived",
  "chat.groups.pinned",
  "chat.groups.projects",
  "chat.groups.today",
  "chat.groups.yesterday",
  "chat.groups.earlier",
  "chat.groups.archived",
  "workbench.tabAria",
  "workbench.panesInTab",
  "workbench.collapseTabGroup",
  "workbench.expandTabGroup",
  "workbench.dropPane",
  "workbench.createGroup",
  "workbench.moveTo",
  "workbench.renameGroupTitle",
  "workbench.renameGroupDescription",
  "workbench.renameGroupPlaceholder",
  "workbench.dissolveTab",
  "workbench.deleteConversations",
  "workbench.paneLimit",
  "workbench.paneActions",
  "workbench.detachPane",
  "workbench.composerAria",
  "thread.promptNavigator.railAria",
  "thread.composer.mentions.cliTitle",
  "thread.composer.mentions.mcpTitle",
  "message.openLink",
  "message.openAttachment",
  "message.skill",
  "settings.channels.connectionChecks",
  "settings.channels.open",
  "recovery.actionFailed",
  "recovery.interrupted",
  "recovery.completed",
  "recovery.failed",
  "recovery.failedHelp",
  "recovery.resuming",
  "recovery.review",
  "recovery.safeResume",
  "recovery.dismiss",
  "recovery.continue",
];
const ACCIDENTALLY_SPANISH_SETTINGS_KEYS = [
  "settings.help.provider",
  "settings.help.configPath",
  "settings.help.selectedPreset",
  "settings.help.maxResults",
  "settings.help.timeout",
  "settings.help.jinaReader",
  "settings.help.imageGeneration",
  "settings.help.imageProvider",
  "settings.help.imageProviderStatus",
  "settings.help.imageModel",
  "settings.help.defaultAspectRatio",
  "settings.help.timezone",
  "settings.help.securityManagedControls",
  "settings.help.selectedModelProvider",
  "settings.help.selectedModelValue",
  "settings.help.cliAppsCatalog",
  "settings.help.cliAppsFilter",
  "settings.help.logs",
  "settings.help.diagnostics",
  "settings.help.localServiceAccessNative",
  "settings.help.webuiDefaultAccessNative",
  "settings.status.savedRestart",
  "settings.status.restartAfterSaving",
  "settings.status.savedRestartApply",
  "settings.status.imageProviderRestart",
  "settings.status.hostRestartAfterSaving",
  "settings.status.hostRestartPending",
  "settings.status.hostApiUnavailable",
  "settings.status.logsOpened",
  "settings.status.logsOpenFailed",
  "settings.status.diagnosticsExported",
  "settings.status.diagnosticsExportFailed",
  "settings.image.missingCredential",
  "settings.oauth.signInHelp",
];
const INDEX_HTML = readFileSync(resolve(process.cwd(), "index.html"), "utf8");
const PREBOOT_SCRIPT = INDEX_HTML.match(
  /<script>\s*(\(function \(\) \{\s*var localeKey = "nanobot\.locale";[\s\S]*?\}\)\(\);)\s*<\/script>/,
)?.[1];
const BOOT_COPY_MARKUP = '<span data-boot-copy>Loading nanobot…</span>';

function runPrebootLocale(storedLocale: string) {
  if (!PREBOOT_SCRIPT) throw new Error("Could not find the preboot locale script in index.html");

  const documentElement = { lang: "" };
  const meta = {
    content: "",
    setAttribute(name: string, value: string) {
      if (name === "content") this.content = value;
    },
  };
  const boot = { textContent: "" };

  runInNewContext(PREBOOT_SCRIPT, {
    localStorage: {
      getItem: (key: string) => key === LOCALE_STORAGE_KEY ? storedLocale : null,
    },
    navigator: { languages: [], language: "en" },
    document: {
      documentElement,
      querySelector: (selector: string) => {
        if (selector === '[data-i18n-meta="description"]') return meta;
        if (selector === "[data-boot-copy]") return boot;
        return null;
      },
    },
  });

  return {
    lang: documentElement.lang,
    boot: boot.textContent,
    description: meta.content,
  };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === "object" && !Array.isArray(value);
}

function flattenResource(value: unknown, prefix = ""): Map<string, unknown> {
  const out = new Map<string, unknown>();
  if (!isRecord(value)) return out;
  for (const [key, child] of Object.entries(value)) {
    const path = prefix ? `${prefix}.${key}` : key;
    if (isRecord(child)) {
      for (const [childPath, childValue] of flattenResource(child, path)) {
        out.set(childPath, childValue);
      }
    } else {
      out.set(path, child);
    }
  }
  return out;
}

function interpolationKeys(value: unknown): string[] {
  if (typeof value !== "string") return [];
  return Array.from(value.matchAll(/{{\s*([\w.-]+)\s*}}/g))
    .map((match) => match[1])
    .sort();
}

describe("webui i18n", () => {
  it("runs preboot localization after the splash copy exists", () => {
    expect(INDEX_HTML.indexOf(BOOT_COPY_MARKUP)).toBeGreaterThan(-1);
    expect(INDEX_HTML.indexOf(PREBOOT_SCRIPT ?? "")).toBeGreaterThan(
      INDEX_HTML.indexOf(BOOT_COPY_MARKUP),
    );
  });

  it("keeps preboot copy aligned with every registered locale", () => {
    for (const { code } of supportedLocales) {
      const result = runPrebootLocale(code);
      const expected = resources[code].common.app;

      expect({ code, ...result }).toEqual({
        code,
        lang: code,
        boot: expected.loading.boot,
        description: expected.meta.description,
      });
    }
  });

  it("normalizes Portuguese locales before the app bundle loads", () => {
    const expected = resources["pt-BR"].common.app;

    for (const locale of ["pt", "pt-PT"]) {
      expect(runPrebootLocale(locale)).toEqual({
        lang: "pt-BR",
        boot: expected.loading.boot,
        description: expected.meta.description,
      });
    }
  });

  it("defaults to English until the user chooses another language", () => {
    localStorage.removeItem(LOCALE_STORAGE_KEY);
    expect(resolveInitialLocale()).toBe("en");

    localStorage.setItem(LOCALE_STORAGE_KEY, "zh-CN");
    expect(resolveInitialLocale()).toBe("zh-CN");
  });

  it("switches UI copy and document locale through the language switcher", async () => {
    const user = userEvent.setup();

    render(
      <>
        <LanguageSwitcher />
        <ThreadComposer onSend={vi.fn()} />
      </>,
    );

    expect(
      screen.getByPlaceholderText("Type your message…"),
    ).toBeInTheDocument();
    expect(document.documentElement.lang).toBe("en");

    await user.click(screen.getByRole("button", { name: "Change language" }));
    await user.click(screen.getByRole("menuitemradio", { name: /简体中文/i }));

    await waitFor(() => {
      expect(document.documentElement.lang).toBe("zh-CN");
    });
    expect(localStorage.getItem("nanobot.locale")).toBe("zh-CN");
    expect(screen.getByPlaceholderText("输入消息…")).toBeInTheDocument();
  });

  it("updates the composer aria label when the language changes", async () => {
    render(<ThreadComposer onSend={vi.fn()} />);

    await act(async () => {
      const { setAppLanguage } = await import("@/i18n");
      await setAppLanguage("ja");
    });

    expect(screen.getByLabelText("メッセージ入力欄")).toBeInTheDocument();
  });

  it("keeps empty landing resources localized for every registered locale", () => {
    for (const resource of Object.values(resources)) {
      const empty = resource.common.thread.empty;
      for (const key of HERO_GREETING_KEYS) {
        expect(empty.greetings[key as keyof typeof empty.greetings]).toBeTruthy();
      }
      for (const key of QUICK_ACTION_KEYS) {
        const action = empty.quickActions[key as keyof typeof empty.quickActions];
        expect(action.title).toBeTruthy();
        expect(action.prompt).toBeTruthy();
      }
      for (const key of IMAGE_QUICK_ACTION_KEYS) {
        const action = empty.imageQuickActions[key as keyof typeof empty.imageQuickActions];
        expect(action.title).toBeTruthy();
        expect(action.prompt).toBeTruthy();
      }
    }
  });

  it("keeps every locale aligned with the English resource shape", () => {
    const reference = flattenResource(resources.en.common);
    for (const [locale, resource] of Object.entries(resources)) {
      if (locale === "en") continue;
      const current = flattenResource(resource.common);
      const missing = Array.from(reference.keys()).filter((key) => !current.has(key));
      const extra = Array.from(current.keys()).filter((key) => !reference.has(key));
      const interpolationMismatches = Array.from(reference.entries())
        .filter(([key]) => current.has(key))
        .filter(([key, value]) =>
          interpolationKeys(value).join(",") !== interpolationKeys(current.get(key)).join(",")
        )
        .map(([key]) => key);

      expect({ locale, missing, extra, interpolationMismatches }).toEqual({
        locale,
        missing: [],
        extra: [],
        interpolationMismatches: [],
      });
    }
  });

  it("keeps slash commands localized for every registered locale", () => {
    for (const resource of Object.values(resources)) {
      const slash = resource.common.thread.composer.slash;
      expect(slash.badges.current).toBeTruthy();
      expect(slash.badges.recent).toBeTruthy();
      expect(slash.details.goalActive).toBeTruthy();
      expect(slash.details.goalReady).toBeTruthy();
      expect(slash.details.history).toBeTruthy();
      expect(slash.details.stopRunning).toBeTruthy();
      for (const key of SLASH_COMMAND_KEYS) {
        const command = slash.commands[key as keyof typeof slash.commands];
        expect(command.title).toBeTruthy();
        expect(command.description).toBeTruthy();
      }
    }
  });

  it("keeps settings navigation localized for every registered locale", () => {
    for (const resource of Object.values(resources)) {
      const common = resource.common;
      expect(common.app.system.restarting).toBeTruthy();
      expect(common.sidebar.settings).toBeTruthy();
      expect(common.chat.showMore).toBeTruthy();
      expect(common.settings.sidebar.title).toBeTruthy();
      expect(common.settings.backToChat).toBeTruthy();
      for (const key of SETTINGS_NAV_KEYS) {
        expect(common.settings.nav[key as keyof typeof common.settings.nav]).toBeTruthy();
      }
      expect(common.settings.sections.about).toBeTruthy();
      expect(common.settings.rows.theme).toBeTruthy();
      expect(common.settings.status.loading).toBeTruthy();
      expect(common.settings.actions.save).toBeTruthy();
      expect(common.settings.actions.edit).toBeTruthy();
      expect(common.settings.byok.configured).toBeTruthy();
      expect(common.settings.byok.configuredSection).toBeTruthy();
      expect(common.settings.byok.showMore).toBeTruthy();
      expect(common.settings.byok.apiKeyRequired).toBeTruthy();
      expect(common.settings.byok.showApiKey).toBeTruthy();
      expect(common.settings.byok.hideApiKey).toBeTruthy();
      expect(common.settings.byok.configuredKeyHint).toBeTruthy();
      expect(common.settings.about.version).toBeTruthy();
      expect(common.settings.about.checkForUpdates).toBeTruthy();
      expect(common.settings.about.updateAvailable).toContain("{{version}}");
    }
  });

  it("does not leak English settings chrome into localized locales", () => {
    const english = flattenResource(resources.en.common);

    for (const [locale, resource] of Object.entries(resources)) {
      if (locale === "en") continue;
      const current = flattenResource(resource.common);
      const leaked = [
        ...LOCALIZED_SETTINGS_COPY_KEYS,
        ...LOCALIZED_WORKSPACE_COPY_KEYS,
        ...LOCALIZED_CHANNEL_SHELL_KEYS,
        ...LOCALIZED_NEW_SURFACE_KEYS,
      ].filter(
        (key) => current.get(key) === english.get(key),
      );

      expect({ locale, leaked }).toEqual({ locale, leaked: [] });
    }
  });

  it("keeps Simplified Chinese settings overview copy localized", () => {
    const settings = resources["zh-CN"].common.settings;

    expect(settings.nav.browser).toBe("网络");
    expect(settings.sections.webSearch).toBe("网络搜索");
    expect(settings.byok.tabs.webSearch).toBe("网络搜索");
    expect(settings.overview.webSearch).toBe("网络搜索");
    expect(settings.overview.workspace).toBe("工作区");
    expect(settings.skills.installedTab).toBe("已安装");
    expect(settings.skills.discoverTab).toBe("发现");
    expect(settings.skills.marketplaceProviderFilter).toBe("技能来源");
    expect(settings.skills.marketplaceProviderAll).toBe("全部");
    expect(settings.skills.marketplaceSearchPlaceholder).toBe("搜索技能");
    expect(settings.skills.marketplaceTrendingTitle).toBe("各市场热门技能");
  });

  it("keeps the Simplified Chinese group workflow localized", () => {
    const workbench = resources["zh-CN"].common.workbench;

    expect(workbench.tabAria).toBe("分组：{{title}}");
    expect(workbench.createGroup).toBe("创建分组");
    expect(workbench.renameGroupTitle).toBe("重命名分组");
    expect(workbench.renameGroupDescription).toBe("为这个分组命名。");
    expect(workbench.renameGroupPlaceholder).toBe("分组名称");
    expect(workbench.moveTo).toBe("移动到");
    expect(workbench.detachPane).toBe("移出");
  });

  it("keeps Indonesian and Vietnamese settings free of copied Spanish help text", () => {
    const spanish = flattenResource(resources.es.common);

    for (const locale of ["id", "vi"] as const) {
      const current = flattenResource(resources[locale].common);
      const copied = ACCIDENTALLY_SPANISH_SETTINGS_KEYS.filter(
        (key) => current.get(key) === spanish.get(key),
      );
      expect({ locale, copied }).toEqual({ locale, copied: [] });
    }
  });

  it("keeps Brazilian Portuguese settings overview copy localized", () => {
    const settings = resources["pt-BR"].common.settings;
    const sidebar = resources["pt-BR"].common.sidebar;
    const chat = resources["pt-BR"].common.chat;

    expect(sidebar.settings).toBe("Configurações");
    expect(chat.newChat).toBe("Novo tópico");
    expect(settings.nav.browser).toBe("Web");
    expect(settings.sections.webSearch).toBe("Busca na web");
    expect(settings.byok.tabs.webSearch).toBe("Busca na web");
    expect(settings.overview.webSearch).toBe("Busca na web");
    expect(settings.overview.workspace).toBe("Espaço de trabalho");
  });
});
