import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { flushPromises, mount } from "@vue/test-utils";
import { computed, defineComponent, nextTick, ref } from "vue";

import { auth } from "../../../frontend/src/auth.js";
import { parseManifest } from "../../../frontend/src/onboarding/onboardingGuides.js";
import { statusLabel, statusPillClass, taskPillClass } from "../../../frontend/src/onboarding/onboardingLabels.js";
import { SECTION_DEFS, STEP_PREREQS, VAULTWARDEN_TEMP_STEP } from "../../../frontend/src/onboarding/onboardingSections.js";
import { useOnboardingFlow } from "../../../frontend/src/onboarding/useOnboardingFlow.js";
import { useOnboardingGuides } from "../../../frontend/src/onboarding/useOnboardingGuides.js";
import { useOnboardingNavigation } from "../../../frontend/src/onboarding/useOnboardingNavigation.js";
import OnboardingView from "../../../frontend/src/views/OnboardingView.vue";

let mockRoute = { query: {} };

jest.mock("vue-router", () => ({
  useRoute: () => mockRoute,
}), { virtual: true });

function jsonResponse(body, status = 200, statusText = "OK") {
  return new Response(JSON.stringify(body), {
    status,
    statusText,
    headers: { "content-type": "application/json" },
  });
}

function manifestResponse() {
  return jsonResponse({
    files: [
      "vaultwarden/step1_website/default/001-login.png",
      "vaultwarden/step1_website/default/002-create-vault.png",
      "vaultwarden/step1_website/mobile/step-03-phone.webp",
      "/element/step1_web_access/001-start.jpg",
      "bad",
      7,
    ],
  });
}

function readyStatus(overrides = {}) {
  return {
    status: "awaiting_onboarding",
    username: "ada",
    initial_password: "temp-pass",
    initial_password_revealed_at: "",
    blocked: false,
    tasks: [{ task: "keycloak", status: "ok" }],
    onboarding: {
      required_steps: [
        "vaultwarden_master_password",
        "keycloak_password_rotated",
        "element_recovery_key",
        "nextcloud_mail_integration",
        "firefly_password_rotated",
        "wger_password_rotated",
      ],
      optional_steps: ["vaultwarden_browser_extension", "mail_client_setup"],
      completed_steps: ["vaultwarden_master_password"],
      vaultwarden: { matched: true, recovery_email: "ada@example.dev" },
      keycloak: { password_rotation_requested: false },
    },
    ...overrides,
  };
}

function installFetch(handler) {
  global.fetch = jest.fn(async (url, options = {}) => handler(String(url), options));
}

function mountFlow(route = { query: {} }) {
  let flow;
  const wrapper = mount(defineComponent({
    setup() {
      flow = useOnboardingFlow(route);
      return {};
    },
    template: "<div />",
  }));
  return { flow, wrapper };
}

describe("onboarding helpers and flow", () => {
  beforeEach(() => {
    jest.useFakeTimers();
    mockRoute = { query: {} };
    auth.authenticated = false;
    auth.token = "";
    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: { writeText: jest.fn(async () => {}) },
    });
    installFetch((url) => (url.includes("/media/onboarding") ? manifestResponse() : jsonResponse(readyStatus())));
  });

  afterEach(() => {
    jest.useRealTimers();
    jest.restoreAllMocks();
    auth.authenticated = false;
    auth.token = "";
    Reflect.deleteProperty(global, "fetch");
  });

  it("maps labels, static sections, and manifest files", () => {
    expect(statusLabel("pending_email_verification")).toBe("confirm email");
    expect(statusLabel("pending")).toBe("awaiting approval");
    expect(statusLabel("accounts_building")).toBe("accounts building");
    expect(statusLabel("awaiting_onboarding")).toBe("awaiting onboarding");
    expect(statusLabel("ready")).toBe("ready");
    expect(statusLabel("denied")).toBe("rejected");
    expect(statusLabel("")).toBe("unknown");
    expect(statusPillClass("denied")).toBe("pill-bad");
    expect(statusPillClass("ready")).toBe("pill-info");
    expect(statusPillClass("other")).toBe("pill-warn");
    expect(taskPillClass("ok")).toBe("pill-ok");
    expect(taskPillClass("error")).toBe("pill-bad");
    expect(taskPillClass("pending")).toBe("pill-warn");

    expect(STEP_PREREQS.element_recovery_key).toEqual(["keycloak_password_rotated"]);
    expect(SECTION_DEFS.map((section) => section.id)).toContain("vaultwarden");
    expect(VAULTWARDEN_TEMP_STEP.id).toBe("vaultwarden_store_temp_password");

    const parsed = parseManifest([
      "vaultwarden\\step1_website\\default\\002-second.png",
      "vaultwarden/step1_website/default/001-first.png",
      "vaultwarden/step1_website/default/final-shot.png",
      "vaultwarden/step1_website/mobile/step-03-phone.webp",
      "ignored",
      null,
    ]);
    expect(parsed.vaultwarden.step1_website.default.shots.map((shot) => shot.label)).toEqual(["first", "second", "final shot"]);
    expect(parsed.vaultwarden.step1_website.mobile.shots[0].order).toBe(3);
  });

  it("paginates guide groups and handles lightbox state", async () => {
    const done = new Set(["vaultwarden_master_password"]);
    const guides = useOnboardingGuides({
      isStepDone: (id) => done.has(id),
      isStepBlocked: (id) => id === "blocked",
    });
    await guides.loadGuideShots();

    const step = { id: "vaultwarden_browser_extension", guide: { service: "vaultwarden", step: "step1_website", take: 1 } };
    const tailStep = { id: "vaultwarden_mobile_app", guide: { service: "vaultwarden", step: "step1_website", tail: 1 } };
    const section = { steps: [{ id: "vaultwarden_master_password", guide: step.guide }, step, { id: "blocked", guide: step.guide }] };

    expect(guides.guideGroups(step)[0].shots).toHaveLength(1);
    expect(guides.guideGroups(tailStep)[0].shots[0].label).toBe("create vault");
    const fullStep = { id: "vaultwarden_browser_extension", guide: { service: "vaultwarden", step: "step1_website" } };
    const fullGroup = guides.guideGroups(fullStep)[0];
    guides.guideNext(fullStep, fullGroup);
    expect(guides.guideIndex(fullStep, fullGroup)).toBe(1);
    guides.guidePrev(fullStep, fullGroup);
    expect(guides.guideIndex(fullStep, fullGroup)).toBe(0);
    expect(guides.shouldOpenGuide(step, section)).toBe(true);
    guides.guideSet(step, guides.guideGroups(step)[0], 12);
    expect(guides.guideIndex(step, guides.guideGroups(step)[0])).toBe(0);
    guides.openLightbox(guides.guideShot(step, guides.guideGroups(step)[0]));
    expect(guides.lightboxShot.value.url).toContain("001-login");
    guides.closeLightbox();
    expect(guides.lightboxShot.value).toBeNull();

    global.fetch.mockRejectedValueOnce(new Error("offline"));
    await guides.loadGuideShots();
    expect(guides.guideShots.value).toEqual({});
  });

  it("navigates onboarding sections with lock and progress rules", () => {
    const activeSectionId = ref("first");
    const completed = ref(["a"]);
    const sections = computed(() => [
      { id: "first", steps: [{ id: "a" }] },
      { id: "second", steps: [{ id: "b" }] },
      { id: "optional", steps: [{ id: "c" }] },
    ]);
    const nav = useOnboardingNavigation({
      sections,
      activeSectionId,
      isStepDone: (id) => completed.value.includes(id),
      isStepRequired: (id) => id !== "c",
      isStepBlocked: (id) => id === "blocked",
    });

    expect(nav.sectionProgress(sections.value[0])).toBe("1/1 done");
    expect(nav.sectionStatusLabel(sections.value[1])).toBe("active");
    nav.nextSection();
    expect(activeSectionId.value).toBe("second");
    expect(nav.hasPrevSection.value).toBe(true);
    expect(nav.stepCardClass({ id: "c" }).optional).toBe(true);
    expect(nav.sectionProgress(sections.value[2])).toBe("optional");
    nav.selectSection("missing");
    expect(activeSectionId.value).toBe("second");
    nav.prevSection();
    expect(activeSectionId.value).toBe("first");
    completed.value = [];
    nav.selectSection("second");
    expect(activeSectionId.value).toBe("first");
  });

  it("checks status, copies credentials, retries, and confirms steps", async () => {
    installFetch((url) => {
      if (url.includes("/media/onboarding")) return manifestResponse();
      if (url.includes("/retry")) return jsonResponse({ queued: true });
      if (url.includes("/onboarding/attest")) {
        return jsonResponse(readyStatus({
          onboarding: { ...readyStatus().onboarding, completed_steps: ["vaultwarden_master_password", "mail_client_setup"] },
        }));
      }
      if (url.includes("/keycloak-password-rotate")) {
        return jsonResponse(readyStatus({
          onboarding: { ...readyStatus().onboarding, keycloak: { password_rotation_requested: true } },
        }));
      }
      return jsonResponse(readyStatus({ tasks: [{ task: "mail", status: "error" }] }));
    });
    const { flow, wrapper } = mountFlow();
    flow.requestCode.value = "ada~abc";
    await flow.check();

    expect(flow.status.value).toBe("awaiting_onboarding");
    expect(flow.requestUsername.value).toBe("ada");
    expect(flow.showPasswordCard.value).toBe(true);
    expect(flow.vaultwardenLoginEmailLower.value).toBe("ada@example.dev");
    expect(flow.mailAddressLower.value).toBe("ada@bstein.dev");
    expect(flow.stepNote({ id: "firefly_password_rotated" })).toContain("ada@bstein.dev");
    expect(flow.stepPillLabel({ id: "keycloak_password_rotated", action: "confirm" })).toBe("ready");
    expect(flow.stepPillClass({ id: "vaultwarden_browser_extension" })).toBe("pill-info");

    flow.togglePassword();
    expect(flow.revealPassword.value).toBe(true);
    await flow.copyInitialPassword();
    await flow.copyUsername();
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith("temp-pass");
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith("ada");
    jest.advanceTimersByTime(1500);
    expect(flow.passwordCopied.value).toBe(false);

    await flow.retryProvisioning();
    expect(flow.retryMessage.value).toBe("Retry requested. Check again in a moment.");

    await flow.setStepCompletion("mail_client_setup", true);
    expect(flow.isStepDone("mail_client_setup")).toBe(true);

    await flow.requestKeycloakPasswordRotation();
    expect(flow.keycloakPasswordRotationRequested.value).toBe(true);

    wrapper.unmount();
  });

  it("handles auth fallback, rotation checks, blocked steps, and failures", async () => {
    auth.authenticated = true;
    auth.token = "token";
    installFetch((url) => {
      if (url.includes("/media/onboarding")) return manifestResponse();
      if (url.includes("/rotation/check")) return jsonResponse({ rotated: false });
      if (url.includes("/onboarding/attest")) return jsonResponse({ error: "forbidden" }, 403, "Forbidden");
      if (url.includes("/keycloak-password-rotate")) return jsonResponse({ error: "blocked" }, 409, "Conflict");
      if (url.includes("/status")) return jsonResponse(readyStatus({
        onboarding: { ...readyStatus().onboarding, completed_steps: [] },
      }));
      return jsonResponse({});
    });

    const { flow, wrapper } = mountFlow();
    flow.requestCode.value = "ada~abc";
    await flow.check();

    await flow.setStepCompletion("element_recovery_key", true);
    expect(flow.error.value).toBe("");
    await flow.setStepCompletion("vaultwarden_master_password", true);
    expect(flow.error.value).toBe("forbidden");

    flow.error.value = "";
    flow.onboarding.value.completed_steps = ["vaultwarden_master_password", "keycloak_password_rotated", "element_recovery_key"];
    await flow.confirmStep({ id: "firefly_password_rotated", action: "auto" });
    expect(flow.error.value).toContain("Firefly still uses");

    await expect(flow.runRotationCheck("wger")).resolves.toEqual({ rotated: false });

    flow.keycloakPasswordRotationRequested.value = false;
    await flow.requestKeycloakPasswordRotation();
    expect(flow.error.value).toBe("blocked");

    wrapper.unmount();
  });

  it("covers onboarding edge branches without hiding workflow failures", async () => {
    const responses = { rotateCalls: 0 };
    installFetch((url) => {
      if (url.includes("/media/onboarding")) return manifestResponse();
      if (url.includes("/rotation/check")) return jsonResponse({ rotated: true });
      if (url.includes("/onboarding/attest")) return jsonResponse(readyStatus({
        onboarding: { ...readyStatus().onboarding, completed_steps: ["vaultwarden_master_password", "mail_client_setup"] },
      }));
      if (url.includes("/keycloak-password-rotate")) {
        responses.rotateCalls += 1;
        if (responses.rotateCalls === 1) return jsonResponse({ error: "auth denied" }, 403, "Forbidden");
        return jsonResponse(readyStatus({
          onboarding: { ...readyStatus().onboarding, keycloak: { password_rotation_requested: true } },
        }));
      }
      if (url.includes("/status")) return jsonResponse(readyStatus({
        initial_password: "",
        initial_password_revealed_at: "2026-04-21T00:00:00Z",
        onboarding: {
          ...readyStatus().onboarding,
          completed_steps: ["vaultwarden_master_password"],
          vaultwarden: { matched: true, recovery_email: "" },
        },
      }));
      return jsonResponse({});
    });

    const { flow, wrapper } = mountFlow();
    flow.loading.value = true;
    await flow.check();
    expect(fetch).not.toHaveBeenCalledWith("/api/access/request/status", expect.anything());
    flow.loading.value = false;

    await flow.setStepCompletion("vaultwarden_master_password", true);
    expect(flow.error.value).toBe("Request code is missing.");
    await flow.retryProvisioning();
    expect(flow.retryMessage.value).toBe("");

    flow.requestCode.value = "ada~abc";
    await flow.check();
    expect(flow.passwordRevealLocked.value).toBe(true);
    expect(flow.passwordRevealHint.value).toContain("already revealed");
    expect(flow.vaultwardenLoginEmailLower.value).toBe("your recovery email");

    flow.onboarding.value = {
      required_steps: ["vaultwarden_master_password", "keycloak_password_rotated", "manual_required"],
      optional_steps: ["optional_step"],
      completed_steps: [],
      vaultwarden: { matched: false },
    };
    flow.requestUsername.value = "";
    expect(flow.vaultwardenLoginEmail.value).toBe("your @bstein.dev address");
    flow.requestUsername.value = "Ada";
    expect(flow.vaultwardenLoginEmailLower.value).toBe("ada@bstein.dev");
    expect(flow.stepNote({ id: "vaultwarden_master_password" })).toContain("ada@bstein.dev");
    expect(flow.stepNote({ id: "vaultwarden_store_temp_password" })).toContain("temporary Keycloak");
    expect(flow.stepNote({ id: "mail_client_setup" })).toContain("ada@bstein.dev");
    expect(flow.stepNote({ id: "unknown" })).toBe("");
    expect(flow.stepPillLabel({ id: "element_recovery_key", action: "confirm" })).toBe("blocked");
    expect(flow.stepPillLabel({ id: "auto_step", action: "auto" })).toBe("pending");
    expect(flow.stepPillLabel({ id: "optional_step", action: "checkbox" })).toBe("optional");
    expect(flow.stepPillLabel({ id: "manual_required", action: "checkbox" })).toBe("pending");
    expect(flow.stepPillClass({ id: "element_recovery_key" })).toBe("pill-wait");
    flow.onboarding.value.completed_steps = ["vaultwarden_master_password"];
    expect(flow.stepPillClass({ id: "keycloak_password_rotated" })).toBe("pill-info");
    flow.keycloakPasswordRotationRequested.value = true;
    expect(flow.stepPillLabel({ id: "keycloak_password_rotated", action: "confirm" })).toBe("rotate now");
    expect(flow.stepPillClass({ id: "keycloak_password_rotated" })).toBe("pill-warn");

    flow.confirmingStepId.value = "manual_required";
    expect(flow.isConfirming({ id: "manual_required" })).toBe(true);
    expect(flow.confirmLabel({ id: "manual_required" })).toBe("Confirming...");
    flow.confirmingStepId.value = "";

    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: { writeText: jest.fn(async () => { throw new Error("copy denied"); }) },
    });
    await flow.copyText("secret", () => {});
    expect(flow.error.value).toBe("copy denied");
    await flow.copyText("", () => { throw new Error("should not run"); });

    await expect(flow.runRotationCheck("firefly")).rejects.toThrow("Log in");
    auth.authenticated = true;
    auth.token = "token";
    await expect(flow.runRotationCheck("wger")).resolves.toEqual({ rotated: true });
    global.fetch.mockResolvedValueOnce(jsonResponse({ error: "rotation down" }, 500, "Server Error"));
    await expect(flow.runRotationCheck("wger")).rejects.toThrow("rotation down");

    flow.onboarding.value.completed_steps = ["vaultwarden_master_password"];
    flow.keycloakPasswordRotationRequested.value = false;
    await flow.requestKeycloakPasswordRotation();
    expect(flow.keycloakPasswordRotationRequested.value).toBe(true);
    await flow.requestKeycloakPasswordRotation();
    expect(responses.rotateCalls).toBe(2);

    await flow.confirmStep(null);
    await flow.confirmStep({ id: "vaultwarden_master_password", action: "confirm" });
    await flow.confirmStep({ id: "keycloak_password_rotated", action: "confirm" });
    flow.onboarding.value.completed_steps = ["vaultwarden_master_password", "keycloak_password_rotated", "element_recovery_key", "firefly_password_rotated"];
    await flow.confirmStep({ id: "wger_password_rotated", action: "auto" });
    await flow.confirmStep({ id: "mail_client_setup", action: "confirm" });
    await flow.confirmStep({ id: "jellyfin_web_access", action: "checkbox" });
    await flow.toggleStep("mail_client_setup", {});

    flow.requestCode.value = "";
    await flow.requestKeycloakPasswordRotation();
    expect(flow.error.value).toBe("Request code is missing.");
    flow.requestCode.value = "ada~abc";
    flow.onboarding.value.completed_steps = [];
    flow.keycloakPasswordRotationRequested.value = false;
    await flow.requestKeycloakPasswordRotation();
    expect(flow.error.value).toBe("Complete earlier onboarding steps first.");

    wrapper.unmount();
  });

  it("renders onboarding page states and guide interactions", async () => {
    mockRoute = { query: { code: "ada~abc" } };
    installFetch((url) => {
      if (url.includes("/media/onboarding")) return manifestResponse();
      if (url.includes("/status")) return jsonResponse(readyStatus());
      if (url.includes("/onboarding/attest")) return jsonResponse(readyStatus({
        onboarding: { ...readyStatus().onboarding, completed_steps: ["vaultwarden_master_password", "vaultwarden_browser_extension"] },
      }));
      return jsonResponse({});
    });

    const wrapper = mount(OnboardingView);
    await flushPromises();

    expect(wrapper.text()).toContain("Onboarding");
    expect(wrapper.text()).toContain("Keycloak temporary credentials");
    expect(wrapper.find("input[readonly]").element.value).toBe("ada");
    expect(wrapper.text()).toContain("Photo guide");

    const revealButton = wrapper.findAll("button.secondary").find((button) => button.text() === "Reveal");
    await revealButton.trigger("click");
    await nextTick();
    expect(wrapper.text()).toContain("Hide");

    const checkbox = wrapper.find("input[type='checkbox']");
    if (checkbox.exists()) {
      await checkbox.setValue(true);
      await flushPromises();
    }

    const img = wrapper.find(".guide-shot");
    if (img.exists()) {
      await img.trigger("click");
      expect(wrapper.find(".lightbox").exists()).toBe(true);
      await wrapper.find(".lightbox button").trigger("click");
      expect(wrapper.find(".lightbox").exists()).toBe(false);
    }

    wrapper.unmount();
  });

  it("renders non-onboarding statuses and copy fallback", async () => {
    Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined });
    Object.defineProperty(document, "execCommand", { configurable: true, value: jest.fn(() => true) });
    installFetch((url) => {
      if (url.includes("/media/onboarding")) return manifestResponse();
      if (url.includes("/status")) return jsonResponse(readyStatus({
        status: "accounts_building",
        blocked: true,
        tasks: [{ task: "mail", status: "error", detail: "smtp" }],
      }));
      if (url.includes("/retry")) return jsonResponse({ error: "retry failed" }, 500, "Server Error");
      return jsonResponse({});
    });

    const { flow, wrapper } = mountFlow({ query: { request_code: "ada~abc" } });
    await flushPromises();
    expect(flow.blocked.value).toBe(true);

    await flow.copyText("manual", () => {});
    expect(document.execCommand).toHaveBeenCalledWith("copy");

    await flow.retryProvisioning();
    expect(flow.retryMessage.value).toBe("retry failed");

    global.fetch.mockResolvedValueOnce(jsonResponse({ error: "status failed" }, 500, "Server Error"));
    await flow.check();
    expect(flow.error.value).toBe("status failed");

    wrapper.unmount();
  });
});
