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

import { useRequestAccessFlow } from "../../../frontend/src/request-access/useRequestAccessFlow.js";
import RequestAccessView from "../../../frontend/src/views/RequestAccessView.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 mountFlow(route = { query: {} }) {
  let flow;
  const wrapper = mount(defineComponent({
    setup() {
      flow = useRequestAccessFlow(route);
      return {};
    },
    template: "<div />",
  }));
  return { flow, wrapper };
}

async function flushTimersAndPromises(ms = 0) {
  if (ms) jest.advanceTimersByTime(ms);
  await Promise.resolve();
  await flushPromises();
}

describe("request access flow", () => {
  beforeEach(() => {
    jest.useFakeTimers();
    mockRoute = { query: {} };
    global.fetch = jest.fn();
    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: { writeText: jest.fn(async () => {}) },
    });
  });

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

  it("maps backend statuses to readable labels and pill classes", () => {
    const { flow, wrapper } = mountFlow();

    expect(flow.statusLabel("pending_email_verification")).toBe("confirm email");
    expect(flow.statusLabel("pending")).toBe("awaiting approval");
    expect(flow.statusLabel("accounts_building")).toBe("accounts building");
    expect(flow.statusLabel("awaiting_onboarding")).toBe("awaiting onboarding");
    expect(flow.statusLabel("ready")).toBe("ready");
    expect(flow.statusLabel("denied")).toBe("rejected");
    expect(flow.statusLabel("custom")).toBe("custom");
    expect(flow.statusLabel("")).toBe("unknown");

    expect(flow.statusPillClass("pending_email_verification")).toBe("pill-warn");
    expect(flow.statusPillClass("pending")).toBe("pill-wait");
    expect(flow.statusPillClass("accounts_building")).toBe("pill-warn");
    expect(flow.statusPillClass("awaiting_onboarding")).toBe("pill-ok");
    expect(flow.statusPillClass("ready")).toBe("pill-info");
    expect(flow.statusPillClass("denied")).toBe("pill-bad");
    expect(flow.statusPillClass("custom")).toBe("pill-warn");
    expect(flow.taskPillClass("ok")).toBe("pill-ok");
    expect(flow.taskPillClass("error")).toBe("pill-bad");
    expect(flow.taskPillClass("pending")).toBe("pill-warn");
    expect(flow.taskPillClass("other")).toBe("pill-warn");

    wrapper.unmount();
  });

  it("checks username availability and handles validation states", async () => {
    const { flow, wrapper } = mountFlow();

    flow.form.username = "ab";
    await nextTick();
    expect(flow.availability.label).toBe("invalid");
    expect(flow.availability.blockSubmit).toBe(true);

    flow.form.username = "bad name";
    await nextTick();
    expect(flow.availability.label).toBe("invalid");
    expect(flow.availability.detail).toContain("letters");

    fetch
      .mockResolvedValueOnce(jsonResponse({ available: true }))
      .mockResolvedValueOnce(jsonResponse({ available: false, reason: "exists" }))
      .mockResolvedValueOnce(jsonResponse({ available: false, reason: "requested", status: "pending" }))
      .mockResolvedValueOnce(jsonResponse({ available: false, reason: "invalid", detail: "Nope" }))
      .mockResolvedValueOnce(jsonResponse({}, 503, "Unavailable"));

    flow.form.username = "alice";
    await nextTick();
    await flushPromises();
    await flushTimersAndPromises(350);
    expect(flow.availability.label).toBe("available");
    expect(flow.availability.detail).toBe("Username is available.");
    expect(flow.availability.checking).toBe(false);

    flow.form.username = "taken";
    await nextTick();
    await flushTimersAndPromises(350);
    expect(flow.availability.label).toBe("taken");
    expect(flow.availability.blockSubmit).toBe(true);

    flow.form.username = "pending";
    await nextTick();
    await flushTimersAndPromises(350);
    expect(flow.availability.label).toBe("requested");
    expect(flow.availability.detail).toBe("Existing request: awaiting approval");

    flow.form.username = "invalid";
    await nextTick();
    await flushTimersAndPromises(350);
    expect(flow.availability.label).toBe("invalid");
    expect(flow.availability.detail).toBe("Nope");

    flow.form.username = "offline";
    await nextTick();
    await flushTimersAndPromises(350);
    expect(flow.availability.label).toBe("error");

    flow.form.username = "";
    await nextTick();
    expect(flow.availability.label).toBe("");

    wrapper.unmount();
  });

  it("submits new requests and copies the request code", async () => {
    const { flow, wrapper } = mountFlow();
    fetch.mockResolvedValueOnce(jsonResponse({ request_code: "alice~abc", status: "pending_email_verification" }));

    flow.form.username = " alice ";
    flow.form.first_name = " Ada ";
    flow.form.last_name = " Lovelace ";
    flow.form.email = " ada@example.dev ";
    flow.form.note = " hello ";
    await flow.submit();

    expect(fetch).toHaveBeenCalledWith("/api/access/request", expect.objectContaining({ method: "POST" }));
    expect(JSON.parse(fetch.mock.calls[0][1].body)).toEqual({
      username: "alice",
      first_name: "Ada",
      last_name: "Lovelace",
      email: "ada@example.dev",
      note: "hello",
    });
    expect(flow.submitted.value).toBe(true);
    expect(flow.requestCode.value).toBe("alice~abc");

    await flow.copyRequestCode();
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith("alice~abc");
    expect(flow.copied.value).toBe(true);
    jest.advanceTimersByTime(1500);
    expect(flow.copied.value).toBe(false);

    navigator.clipboard.writeText.mockRejectedValueOnce(new Error("clipboard denied"));
    await flow.copyRequestCode();
    expect(flow.error.value).toBe("clipboard denied");

    wrapper.unmount();
  });

  it("falls back to textarea copy and reports submit failures", async () => {
    const { flow, wrapper } = mountFlow();
    Object.defineProperty(document, "execCommand", {
      configurable: true,
      value: jest.fn(() => true),
    });
    Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined });

    fetch.mockResolvedValueOnce(jsonResponse({ error: "bad request" }, 400, "Bad Request"));
    await flow.submit();
    expect(flow.error.value).toBe("bad request");

    flow.requestCode.value = "bob~code";
    await flow.copyRequestCode();
    expect(document.execCommand).toHaveBeenCalledWith("copy");
    expect(flow.copied.value).toBe(true);

    wrapper.unmount();
  });

  it("checks status, retry tasks, and resend verification flows", async () => {
    const { flow, wrapper } = mountFlow();
    flow.statusForm.request_code = "alice~abc";
    fetch
      .mockResolvedValueOnce(jsonResponse({
        status: "pending",
        email_verified: true,
        onboarding_url: "/onboarding?code=alice",
        blocked: true,
        tasks: [
          { task: "keycloak", status: "error", detail: "boom" },
          { task: "mail", status: "ok" },
        ],
      }))
      .mockResolvedValueOnce(jsonResponse({ retry: "queued" }))
      .mockResolvedValueOnce(jsonResponse({ status: "accounts_building", tasks: [] }))
      .mockResolvedValueOnce(jsonResponse({ sent: true }));

    await flow.checkStatus();
    expect(flow.status.value).toBe("pending");
    expect(flow.verifyBanner.value.title).toBe("Email confirmed");
    expect(flow.onboardingUrl.value).toBe("/onboarding?code=alice");
    expect(flow.blocked.value).toBe(true);

    await flow.retryProvisioning();
    expect(JSON.parse(fetch.mock.calls[1][1].body)).toEqual({ request_code: "alice~abc", tasks: ["keycloak"] });
    expect(flow.retryMessage.value).toBe("Retry requested. Check again in a moment.");
    expect(flow.status.value).toBe("accounts_building");

    flow.status.value = "pending_email_verification";
    await flow.resendVerification();
    expect(flow.resendMessage.value).toBe("Verification email sent.");

    wrapper.unmount();
  });

  it("reports invalid status, retry, and resend failures", async () => {
    const { flow, wrapper } = mountFlow();

    flow.statusForm.request_code = "not-a-code";
    await flow.checkStatus();
    expect(flow.error.value).toContain("Request code should look like");
    expect(flow.status.value).toBe("unknown");

    flow.statusForm.request_code = "alice~abc";
    fetch
      .mockResolvedValueOnce(jsonResponse({ error: "status failed" }, 500, "Server Error"))
      .mockResolvedValueOnce(jsonResponse({ error: "retry failed" }, 429, "Too Many Requests"))
      .mockResolvedValueOnce(jsonResponse({ error: "resend failed" }, 429, "Too Many Requests"));

    await flow.checkStatus();
    expect(flow.error.value).toBe("status failed");
    expect(flow.tasks.value).toEqual([]);

    flow.tasks.value = [{ task: "mail", status: "error" }];
    await flow.retryProvisioning();
    expect(flow.retryMessage.value).toBe("retry failed");

    await flow.resendVerification();
    expect(flow.resendMessage.value).toBe("resend failed");

    wrapper.unmount();
  });

  it("processes verification links during mount", async () => {
    fetch
      .mockResolvedValueOnce(jsonResponse({ status: "pending" }))
      .mockResolvedValueOnce(jsonResponse({ status: "pending", email_verified: true, tasks: [] }));

    const { flow, wrapper } = mountFlow({
      query: {
        code: "alice~abc",
        verify: "token",
        verified: "1",
      },
    });
    await flushPromises();

    expect(flow.submitted.value).toBe(true);
    expect(flow.requestCode.value).toBe("alice~abc");
    expect(flow.verifyBanner.value.title).toBe("Email confirmed");
    expect(fetch).toHaveBeenCalledWith("/api/access/request/verify", expect.objectContaining({ method: "POST" }));

    wrapper.unmount();
  });

  it("renders the request access view states", async () => {
    mockRoute = { query: {} };
    fetch
      .mockResolvedValueOnce(jsonResponse({ available: true }))
      .mockResolvedValueOnce(jsonResponse({ request_code: "alice~abc", status: "pending_email_verification" }))
      .mockResolvedValueOnce(jsonResponse({
        status: "awaiting_onboarding",
        onboarding_url: "/onboarding?code=alice",
        blocked: true,
        tasks: [{ task: "mail", status: "error", detail: "smtp offline" }],
      }));

    const wrapper = mount(RequestAccessView);
    expect(wrapper.text()).toContain("Request Access");

    await wrapper.find("input[autocomplete='username']").setValue("alice");
    await flushTimersAndPromises(350);
    expect(wrapper.text()).toContain("available");

    await wrapper.find("input[autocomplete='family-name']").setValue("Lovelace");
    await wrapper.find("input[autocomplete='given-name']").setValue("Ada");
    await wrapper.find("input[autocomplete='email']").setValue("ada@example.dev");
    await wrapper.find("textarea").setValue("Please approve");
    await wrapper.find("form").trigger("submit.prevent");
    await flushPromises();
    expect(wrapper.text()).toContain("Request submitted.");
    expect(wrapper.text()).toContain("alice~abc");

    await wrapper.find(".status-form input").setValue("alice~abc");
    await wrapper.find(".status-form button").trigger("click");
    await flushPromises();
    expect(wrapper.text()).toContain("Automation");
    expect(wrapper.text()).toContain("smtp offline");
    expect(wrapper.text()).toContain("Continue onboarding");

    wrapper.unmount();
  });

  it("renders verification errors from route query", async () => {
    mockRoute = { query: { code: "alice~abc", verify_error: "expired%20token" } };
    fetch.mockResolvedValueOnce(jsonResponse({ status: "unknown", tasks: [] }));

    const wrapper = shallowMount(RequestAccessView);
    await flushPromises();

    expect(wrapper.text()).toContain("Email verification failed: expired token");
    wrapper.unmount();
  });
});
