import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ClientDetailResponse } from "../../lib/devApi";
import ClientGeneralPage from "./ClientGeneralPage";

const navigateMock = vi.fn();
const fetchClientMock = vi.fn();
const updateClientMock = vi.fn();
const fetchClientRelationsMock = vi.fn();
const fetchTenantsMock = vi.fn();
const fetchMeMock = vi.fn();

let authState = {
  user: {
    access_token: "access-token",
    profile: {
      sub: "admin-user",
      role: "super_admin",
      name: "Dev Admin",
    },
  },
};

vi.mock("react-oidc-context", () => ({
  useAuth: () => authState,
}));

vi.mock("react-router-dom", async () => {
  const actual =
    await vi.importActual<typeof import("react-router-dom")>(
      "react-router-dom",
    );
  return {
    ...actual,
    useNavigate: () => navigateMock,
  };
});

vi.mock("../../lib/devApi", () => ({
  createClient: vi.fn(),
  deleteClient: vi.fn(),
  fetchClient: (...args: unknown[]) => fetchClientMock(...args),
  fetchClientRelations: (...args: unknown[]) =>
    fetchClientRelationsMock(...args),
  fetchTenants: (...args: unknown[]) => fetchTenantsMock(...args),
  refreshHeadlessJwksCache: vi.fn(),
  revokeHeadlessJwksCache: vi.fn(),
  updateClient: (...args: unknown[]) => updateClientMock(...args),
  updateClientStatus: vi.fn(),
}));

vi.mock("../auth/authApi", () => ({
  fetchMe: (...args: unknown[]) => fetchMeMock(...args),
}));

vi.mock("../../lib/i18n", () => ({
  t: (key: string, fallback?: string, vars?: Record<string, unknown>) => {
    let text = fallback ?? key;
    for (const [name, value] of Object.entries(vars ?? {})) {
      text = text.replaceAll(`{{${name}}}`, String(value));
    }
    return text;
  },
}));

const roots: Root[] = [];

function makeClientDetail(
  claimKey: string,
  options?: {
    includeTenantScope?: boolean;
    tenantAccessRestricted?: boolean;
    tenantScopeMandatory?: boolean;
  },
): ClientDetailResponse {
  const includeTenantScope = options?.includeTenantScope ?? false;
  const tenantAccessRestricted = options?.tenantAccessRestricted ?? false;
  const tenantScopeMandatory = options?.tenantScopeMandatory ?? false;
  const structuredScopes = [
    {
      id: "1",
      name: "openid",
      description: "",
      mandatory: true,
    },
  ];

  if (includeTenantScope) {
    structuredScopes.push({
      id: "2",
      name: "tenant",
      description: "Tenant access",
      mandatory: tenantScopeMandatory,
      locked: tenantAccessRestricted,
    });
  }

  return {
    client: {
      id: "client-claims",
      name: "Claims App",
      type: "private",
      status: "active",
      redirectUris: ["https://rp.example.com/callback"],
      scopes: includeTenantScope
        ? ["openid", "tenant", "profile"]
        : ["openid", "profile"],
      tokenEndpointAuthMethod: "client_secret_basic",
      metadata: {
        description: "Claims app",
        tenant_access_restricted: tenantAccessRestricted,
        structured_scopes: structuredScopes,
        id_token_claims: [
          {
            namespace: "rp_claims",
            key: claimKey,
            value: "A",
            valueType: "text",
            readPermission: "admin_only",
            writePermission: "admin_only",
          },
        ],
      },
    },
    endpoints: {
      discovery: "https://issuer/.well-known/openid-configuration",
      issuer: "https://issuer",
      authorization: "https://issuer/oauth2/auth",
      token: "https://issuer/oauth2/token",
      userinfo: "https://issuer/userinfo",
    },
  };
}

async function flush() {
  await act(async () => {
    await new Promise((resolve) => setTimeout(resolve, 0));
  });
}

async function setInputValue(input: HTMLInputElement, value: string) {
  const descriptor = Object.getOwnPropertyDescriptor(
    HTMLInputElement.prototype,
    "value",
  );
  descriptor?.set?.call(input, value);
  input.dispatchEvent(new Event("input", { bubbles: true }));
  await flush();
}

async function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
  const descriptor = Object.getOwnPropertyDescriptor(
    HTMLTextAreaElement.prototype,
    "value",
  );
  descriptor?.set?.call(textarea, value);
  textarea.dispatchEvent(new Event("input", { bubbles: true }));
  await flush();
}

async function setSelectValue(select: HTMLSelectElement, value: string) {
  const descriptor = Object.getOwnPropertyDescriptor(
    HTMLSelectElement.prototype,
    "value",
  );
  descriptor?.set?.call(select, value);
  select.dispatchEvent(new Event("change", { bubbles: true }));
  await flush();
}

async function renderPage() {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const root = createRoot(container);
  roots.push(root);
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });

  await act(async () => {
    root.render(
      <QueryClientProvider client={queryClient}>
        <MemoryRouter initialEntries={["/clients/client-claims/settings"]}>
          <Routes>
            <Route
              path="/clients/:id/settings"
              element={<ClientGeneralPage />}
            />
          </Routes>
        </MemoryRouter>
      </QueryClientProvider>,
    );
  });
  await flush();

  return { container, queryClient };
}

describe("ClientGeneralPage RP claims", () => {
  beforeEach(() => {
    authState = {
      user: {
        access_token: "access-token",
        profile: {
          sub: "admin-user",
          role: "super_admin",
          name: "Dev Admin",
        },
      },
    };
    fetchClientMock.mockResolvedValue(makeClientDetail("old_claim"));
    updateClientMock.mockResolvedValue(makeClientDetail("new_claim"));
    fetchClientRelationsMock.mockResolvedValue({ items: [] });
    fetchTenantsMock.mockResolvedValue({
      items: [],
      limit: 1000,
      offset: 0,
      total: 0,
    });
    fetchMeMock.mockResolvedValue({
      id: "admin-user",
      role: "super_admin",
      name: "Dev Admin",
    });
    navigateMock.mockReset();
  });

  afterEach(() => {
    for (const root of roots.splice(0)) {
      act(() => {
        root.unmount();
      });
    }
    vi.restoreAllMocks();
    vi.clearAllMocks();
    document.body.innerHTML = "";
  });

  it("updates the client detail cache with saved RP claims before stale data can rehydrate the form", async () => {
    const { container, queryClient } = await renderPage();

    const claimKeyInput = container.querySelector<HTMLInputElement>(
      'input[placeholder="e.g. locale"]',
    );
    expect(claimKeyInput).not.toBeNull();
    expect(claimKeyInput?.value).toBe("old_claim");

    await setInputValue(claimKeyInput as HTMLInputElement, "new_claim");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    const cached = queryClient.getQueryData<ClientDetailResponse>([
      "client",
      "client-claims",
    ]);
    expect(cached?.client.metadata?.id_token_claims).toEqual([
      {
        namespace: "rp_claims",
        key: "new_claim",
        value: "A",
        valueType: "text",
        readPermission: "admin_only",
        writePermission: "admin_only",
      },
    ]);
  });

  it("forces user read permission on when user write permission is enabled for RP claims", async () => {
    const { container } = await renderPage();

    const switches = Array.from(
      container.querySelectorAll<HTMLButtonElement>('[role="switch"]'),
    );
    const readSwitch = switches.find((button) =>
      /Read|읽기/.test(button.getAttribute("aria-label") ?? ""),
    );
    const writeSwitch = switches.find((button) =>
      /Write|쓰기/.test(button.getAttribute("aria-label") ?? ""),
    );

    expect(readSwitch).toBeDefined();
    expect(writeSwitch).toBeDefined();
    expect(readSwitch?.getAttribute("aria-checked")).toBe("false");
    expect(writeSwitch?.getAttribute("aria-checked")).toBe("false");

    await act(async () => {
      writeSwitch?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(readSwitch?.getAttribute("aria-checked")).toBe("true");
    expect(writeSwitch?.getAttribute("aria-checked")).toBe("true");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).toHaveBeenCalledWith(
      "client-claims",
      expect.objectContaining({
        metadata: expect.objectContaining({
          id_token_claims: [
            expect.objectContaining({
              readPermission: "user_and_admin",
              writePermission: "user_and_admin",
            }),
          ],
        }),
      }),
    );
  });

  it("preserves tenant scope mandatory state when tenant access restriction is off", async () => {
    fetchClientMock.mockResolvedValue(
      makeClientDetail("old_claim", {
        includeTenantScope: true,
        tenantAccessRestricted: false,
        tenantScopeMandatory: true,
      }),
    );
    updateClientMock.mockResolvedValue(
      makeClientDetail("old_claim", {
        includeTenantScope: true,
        tenantAccessRestricted: false,
        tenantScopeMandatory: false,
      }),
    );

    const { container } = await renderPage();

    const tenantScopeRow = Array.from(container.querySelectorAll("tr")).find(
      (row) =>
        Array.from(row.querySelectorAll("input")).some(
          (input) => (input as HTMLInputElement).value === "tenant",
        ),
    );

    expect(tenantScopeRow).toBeDefined();
    const mandatorySwitch =
      tenantScopeRow?.querySelector<HTMLButtonElement>('[role="switch"]');
    expect(mandatorySwitch).toBeDefined();
    expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("true");

    await act(async () => {
      mandatorySwitch?.dispatchEvent(
        new MouseEvent("click", { bubbles: true }),
      );
    });
    await flush();

    expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("false");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).toHaveBeenCalledWith(
      "client-claims",
      expect.objectContaining({
        metadata: expect.objectContaining({
          tenant_access_restricted: false,
          structured_scopes: expect.arrayContaining([
            expect.objectContaining({
              name: "tenant",
              mandatory: false,
              locked: false,
            }),
          ]),
        }),
      }),
    );
  });

  it("keeps nullable and default value as separate RP claim settings", async () => {
    const { container } = await renderPage();

    expect(container.textContent).toContain("Nullable");
    expect(container.textContent).toContain("Default Value");
    expect(container.textContent).not.toContain("Nullable/default");
    expect(container.textContent).toContain(
      "RP 전용 확장 claim을 구분해서 관리합니다",
    );
  });

  it("shows supported scopes including offline_access and custom claims from the add scope button", async () => {
    const { container } = await renderPage();

    const addScopeButton = Array.from(
      container.querySelectorAll("button"),
    ).find((button) => button.textContent?.includes("Scope 추가"));
    expect(addScopeButton).toBeDefined();

    await act(async () => {
      addScopeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(container.textContent).toContain("offline_access");
    expect(container.textContent).toContain("old_claim");

    const customClaimButton = Array.from(
      container.querySelectorAll("button"),
    ).find((button) => button.textContent?.includes("old_claim"));
    expect(customClaimButton).toBeDefined();

    await act(async () => {
      customClaimButton?.dispatchEvent(
        new MouseEvent("click", { bubbles: true }),
      );
    });
    await flush();

    const scopeInputs = Array.from(
      container.querySelectorAll<HTMLInputElement>(
        'input[placeholder="e.g. profile"]',
      ),
    );
    expect(scopeInputs.some((input) => input.value === "old_claim")).toBe(true);
  });

  it("shows the offline_access guide in the scopes section and expands its details", async () => {
    const { container } = await renderPage();

    expect(container.textContent).toContain(
      "Refresh token 사용 시 offline_access scope가 필요합니다.",
    );
    expect(container.textContent).toContain(
      "scope 목록에 offline_access를 포함하고",
    );

    const guideToggleButton = Array.from(
      container.querySelectorAll("button"),
    ).find((button) =>
      (button.getAttribute("aria-label") ?? "").includes("상세 안내 보기"),
    );
    expect(guideToggleButton).toBeDefined();

    await act(async () => {
      guideToggleButton?.dispatchEvent(
        new MouseEvent("click", { bubbles: true }),
      );
    });
    await flush();

    expect(container.textContent).toContain(
      "Hydra 기준으로 refresh token 발급 조건",
    );
    expect(container.textContent).toContain(
      "authorization request scope에 offline 또는 offline_access 포함",
    );
    expect(container.textContent).toContain(
      "consent accept의 granted_scope에 offline 또는 offline_access 포함",
    );
    expect(container.textContent).toContain(
      "client grant_types에 refresh_token 포함",
    );
  });

  it("blocks saving a number RP claim default value that is not numeric", async () => {
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();
    await setSelectValue(valueTypeSelect as HTMLSelectElement, "number");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).not.toHaveBeenCalled();
  });

  it("blocks saving a number RP claim default value that is not an integer", async () => {
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();
    await setSelectValue(valueTypeSelect as HTMLSelectElement, "number");

    const defaultValueInput = container.querySelector<HTMLInputElement>(
      'input[placeholder="Enter the default value"]',
    );
    expect(defaultValueInput).not.toBeNull();
    await setInputValue(defaultValueInput as HTMLInputElement, "3.14");

    expect(container.textContent).toContain(
      "Claim 기본값이 타입과 맞지 않습니다",
    );

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).not.toHaveBeenCalled();
  });

  it("saves a float RP claim default value", async () => {
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();
    expect(
      valueTypeSelect?.querySelector('option[value="float"]'),
    ).not.toBeNull();
    await setSelectValue(valueTypeSelect as HTMLSelectElement, "float");

    const defaultValueInput = container.querySelector<HTMLInputElement>(
      'input[placeholder="Enter the default value"]',
    );
    expect(defaultValueInput).not.toBeNull();
    expect(defaultValueInput?.getAttribute("inputmode")).toBe("decimal");
    await setInputValue(defaultValueInput as HTMLInputElement, "3.14");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).toHaveBeenCalledWith(
      "client-claims",
      expect.objectContaining({
        metadata: expect.objectContaining({
          id_token_claims: [
            expect.objectContaining({
              value: "3.14",
              valueType: "float",
            }),
          ],
        }),
      }),
    );
  });

  it("renders constrained default value controls for boolean and date RP claims", async () => {
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();

    await setSelectValue(valueTypeSelect as HTMLSelectElement, "boolean");
    const booleanDefaultSelect = Array.from(
      container.querySelectorAll<HTMLSelectElement>("select"),
    ).find((select) =>
      Array.from(select.options).some((option) => option.value === "false"),
    );
    expect(booleanDefaultSelect).toBeDefined();

    await setSelectValue(valueTypeSelect as HTMLSelectElement, "date");
    expect(container.querySelector('input[type="date"]')).not.toBeNull();
    expect(
      container.querySelector('select[aria-label="Claim 기본값 시간대"]'),
    ).not.toBeNull();
  });

  it("saves date RP claim default values as Unix seconds for the selected timezone", async () => {
    vi.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({
      locale: "ko-KR",
      calendar: "gregory",
      numberingSystem: "latn",
      timeZone: "Asia/Seoul",
    } as Intl.ResolvedDateTimeFormatOptions);
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();
    await setSelectValue(valueTypeSelect as HTMLSelectElement, "date");

    const defaultValueInput =
      container.querySelector<HTMLInputElement>('input[type="date"]');
    expect(defaultValueInput).not.toBeNull();
    await setInputValue(defaultValueInput as HTMLInputElement, "2026-06-10");

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).toHaveBeenCalledWith(
      "client-claims",
      expect.objectContaining({
        metadata: expect.objectContaining({
          id_token_claims: [
            expect.objectContaining({
              value: 1781017200,
              valueType: "date",
            }),
          ],
        }),
      }),
    );
  });

  it("blocks saving an object RP claim default value that is not a JSON object", async () => {
    const { container } = await renderPage();

    const valueTypeSelect = container.querySelector<HTMLSelectElement>(
      'select[aria-label="Claim 값 타입"]',
    );
    expect(valueTypeSelect).not.toBeNull();
    await setSelectValue(valueTypeSelect as HTMLSelectElement, "object");

    const defaultValueInput = container.querySelector<HTMLTextAreaElement>(
      'textarea[placeholder="{\\"key\\": \\"value\\"}"]',
    );
    expect(defaultValueInput).not.toBeNull();
    await setTextareaValue(
      defaultValueInput as HTMLTextAreaElement,
      "not-json",
    );

    const saveButton = Array.from(container.querySelectorAll("button")).find(
      (button) =>
        button.textContent?.includes("저장") ||
        button.textContent?.includes("Save"),
    );
    expect(saveButton).toBeDefined();

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flush();

    expect(updateClientMock).not.toHaveBeenCalled();
  });
});
