import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { describe, it, expect, vi, beforeEach } from "vitest";
import AdminPage from "./AdminPage";
import * as client from "../api/client";

// ---------------------------------------------------------------------------
// Mock data
// ---------------------------------------------------------------------------

const MOCK_SETTINGS = {
  guest_access_enabled: false,
  default_endpoint_id: "ep1",
  token_budget_daily: 100000,
  token_budget_monthly: 2000000,
  api_keys: [
    {
      id: "key1",
      label: "CI Pipeline",
      prefix: "pl_abc",
      created_at: "2026-04-01T10:00:00Z",
    },
  ],
};

const MOCK_STATS = {
  total_runs: 1234,
  total_experiments: 56,
  total_projects: 8,
  cache_entries: 890,
  cache_hit_rate: 0.73,
  storage_bytes: 52428800,
  tokens_spent: 456789,
};

const MOCK_ENDPOINTS: client.EndpointResponse[] = [
  { id: "ep1", name: "OpenAI", url: "https://api.openai.com", default_model: "gpt-4" },
  { id: "ep2", name: "Local vLLM", url: "http://localhost:8080", default_model: null },
];

const MOCK_WEBHOOKS: client.WebhookResponse[] = [
  {
    id: "wh1",
    event_type: "run.completed",
    url: "https://example.com/hook",
    headers: null,
    is_active: true,
  },
];

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function mockAllApis() {
  vi.spyOn(client.admin, "getSettings").mockResolvedValue(
    MOCK_SETTINGS as unknown as Record<string, unknown>,
  );
  vi.spyOn(client.admin, "getStats").mockResolvedValue(
    MOCK_STATS as unknown as Record<string, unknown>,
  );
  vi.spyOn(client.admin, "updateSettings").mockResolvedValue(
    {} as Record<string, unknown>,
  );
  vi.spyOn(client.endpoints, "list").mockResolvedValue({
    items: MOCK_ENDPOINTS,
    total: MOCK_ENDPOINTS.length,
  });
  vi.spyOn(client.webhooks, "list").mockResolvedValue({
    items: MOCK_WEBHOOKS,
    total: MOCK_WEBHOOKS.length,
  });
  vi.spyOn(client.webhooks, "create").mockResolvedValue({
    id: "wh2",
    event_type: "run.started",
    url: "https://new.com/hook",
    headers: null,
    is_active: true,
  });
  vi.spyOn(client.webhooks, "delete").mockResolvedValue(undefined as never);
}

function renderAdmin() {
  return render(
    <MemoryRouter initialEntries={["/admin"]}>
      <AdminPage />
    </MemoryRouter>,
  );
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("AdminPage", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it("shows loading state initially", () => {
    vi.spyOn(client.admin, "getSettings").mockReturnValue(
      new Promise(() => {}),
    );
    vi.spyOn(client.admin, "getStats").mockReturnValue(new Promise(() => {}));
    vi.spyOn(client.endpoints, "list").mockReturnValue(new Promise(() => {}));
    vi.spyOn(client.webhooks, "list").mockReturnValue(new Promise(() => {}));

    renderAdmin();
    expect(screen.getByText("Loading admin data...")).toBeInTheDocument();
  });

  it("shows error state on API failure", async () => {
    vi.spyOn(client.admin, "getSettings").mockRejectedValue(
      new client.ApiError(500, "Internal Server Error", {}),
    );
    vi.spyOn(client.admin, "getStats").mockRejectedValue(
      new client.ApiError(500, "Internal Server Error", {}),
    );
    vi.spyOn(client.endpoints, "list").mockRejectedValue(
      new client.ApiError(500, "Internal Server Error", {}),
    );
    vi.spyOn(client.webhooks, "list").mockRejectedValue(
      new client.ApiError(500, "Internal Server Error", {}),
    );

    renderAdmin();

    await waitFor(() => {
      expect(screen.getByRole("alert")).toBeInTheDocument();
    });
    expect(screen.getByText(/Failed to load admin data/)).toBeInTheDocument();
  });

  it("shows network error message on non-API errors", async () => {
    vi.spyOn(client.admin, "getSettings").mockRejectedValue(
      new Error("fetch failed"),
    );
    vi.spyOn(client.admin, "getStats").mockResolvedValue({});
    vi.spyOn(client.endpoints, "list").mockResolvedValue({
      items: [],
      total: 0,
    });
    vi.spyOn(client.webhooks, "list").mockResolvedValue({
      items: [],
      total: 0,
    });

    renderAdmin();

    await waitFor(() => {
      expect(
        screen.getByText("Network error. Is the server running?"),
      ).toBeInTheDocument();
    });
  });

  it("renders settings section after loading", async () => {
    mockAllApis();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByText("Settings")).toBeInTheDocument();
    });

    // Guest access toggle (should be off)
    const toggle = screen.getByTestId("guest-access-toggle");
    expect(toggle).toHaveAttribute("aria-checked", "false");

    // Default endpoint should be selected
    const endpointSelect = screen.getByTestId(
      "default-endpoint-select",
    ) as HTMLSelectElement;
    expect(endpointSelect.value).toBe("ep1");

    // Token budgets
    const daily = screen.getByTestId("budget-daily") as HTMLInputElement;
    expect(daily.value).toBe("100000");
    const monthly = screen.getByTestId("budget-monthly") as HTMLInputElement;
    expect(monthly.value).toBe("2000000");
  });

  it("toggles guest access and saves", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("guest-access-toggle")).toBeInTheDocument();
    });

    await user.click(screen.getByTestId("guest-access-toggle"));

    await waitFor(() => {
      expect(client.admin.updateSettings).toHaveBeenCalledWith(
        expect.objectContaining({ guest_access_enabled: true }),
      );
    });
  });

  it("renders system stats", async () => {
    mockAllApis();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByText("System Stats")).toBeInTheDocument();
    });

    expect(screen.getByTestId("stat-total-runs")).toHaveTextContent("1,234");
    expect(screen.getByTestId("stat-total-experiments")).toHaveTextContent("56");
    expect(screen.getByTestId("stat-cache-hit-rate")).toHaveTextContent("73.0%");
    expect(screen.getByTestId("stat-tokens-spent")).toHaveTextContent("456,789");
  });

  it("renders API keys and revoke works", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByText("API Keys")).toBeInTheDocument();
    });

    expect(screen.getByText("CI Pipeline")).toBeInTheDocument();
    expect(screen.getByText(/pl_abc/)).toBeInTheDocument();

    // Revoke key
    await user.click(screen.getByTestId("revoke-key-key1"));

    await waitFor(() => {
      expect(client.admin.updateSettings).toHaveBeenCalledWith(
        expect.objectContaining({
          action: "revoke_api_key",
          key_id: "key1",
        }),
      );
    });

    // Key should be removed from UI
    await waitFor(() => {
      expect(screen.queryByText("CI Pipeline")).not.toBeInTheDocument();
    });
  });

  it("generates a new API key", async () => {
    mockAllApis();
    vi.spyOn(client.admin, "updateSettings").mockResolvedValue({
      api_key: {
        id: "key2",
        label: "Dev Key",
        prefix: "pl_xyz",
        created_at: "2026-04-07T12:00:00Z",
      },
    } as unknown as Record<string, unknown>);

    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("api-key-label")).toBeInTheDocument();
    });

    await user.type(screen.getByTestId("api-key-label"), "Dev Key");
    await user.click(screen.getByTestId("generate-key-btn"));

    await waitFor(() => {
      expect(client.admin.updateSettings).toHaveBeenCalledWith(
        expect.objectContaining({
          action: "generate_api_key",
          label: "Dev Key",
        }),
      );
    });

    await waitFor(() => {
      expect(screen.getByText("Dev Key")).toBeInTheDocument();
    });
  });

  it("renders webhooks and supports delete", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByText("Webhooks")).toBeInTheDocument();
    });

    expect(screen.getByText("1 webhook configured")).toBeInTheDocument();
    expect(screen.getByText("run.completed")).toBeInTheDocument();
    expect(screen.getByText("https://example.com/hook")).toBeInTheDocument();

    // Delete webhook
    await user.click(screen.getByTestId("delete-webhook-wh1"));

    await waitFor(() => {
      expect(client.webhooks.delete).toHaveBeenCalledWith("wh1");
    });

    await waitFor(() => {
      expect(
        screen.queryByText("https://example.com/hook"),
      ).not.toBeInTheDocument();
    });
  });

  it("creates a new webhook", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("add-webhook-btn")).toBeInTheDocument();
    });

    // Open form
    await user.click(screen.getByTestId("add-webhook-btn"));

    await waitFor(() => {
      expect(screen.getByTestId("webhook-form")).toBeInTheDocument();
    });

    await user.type(
      screen.getByTestId("webhook-url-input"),
      "https://new.com/hook",
    );
    await user.click(screen.getByTestId("create-webhook-btn"));

    await waitFor(() => {
      expect(client.webhooks.create).toHaveBeenCalledWith({
        event_type: "run.started",
        url: "https://new.com/hook",
      });
    });

    // New webhook should appear
    await waitFor(() => {
      expect(screen.getByText("https://new.com/hook")).toBeInTheDocument();
    });
  });

  it("retry button reloads data after error", async () => {
    const getSettingsSpy = vi
      .spyOn(client.admin, "getSettings")
      .mockRejectedValueOnce(new client.ApiError(500, "Error", {}))
      .mockResolvedValueOnce(MOCK_SETTINGS as unknown as Record<string, unknown>);

    vi.spyOn(client.admin, "getStats")
      .mockRejectedValueOnce(new client.ApiError(500, "Error", {}))
      .mockResolvedValueOnce(MOCK_STATS as unknown as Record<string, unknown>);
    vi.spyOn(client.endpoints, "list")
      .mockRejectedValueOnce(new client.ApiError(500, "Error", {}))
      .mockResolvedValueOnce({
        items: MOCK_ENDPOINTS,
        total: MOCK_ENDPOINTS.length,
      });
    vi.spyOn(client.webhooks, "list")
      .mockRejectedValueOnce(new client.ApiError(500, "Error", {}))
      .mockResolvedValueOnce({
        items: MOCK_WEBHOOKS,
        total: MOCK_WEBHOOKS.length,
      });

    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByRole("alert")).toBeInTheDocument();
    });

    await user.click(screen.getByText("Retry"));

    await waitFor(() => {
      expect(screen.getByText("Settings")).toBeInTheDocument();
    });

    expect(getSettingsSpy).toHaveBeenCalledTimes(2);
  });

  it("shows empty API keys message when none exist", async () => {
    vi.spyOn(client.admin, "getSettings").mockResolvedValue({
      ...MOCK_SETTINGS,
      api_keys: [],
    } as unknown as Record<string, unknown>);
    vi.spyOn(client.admin, "getStats").mockResolvedValue(
      MOCK_STATS as unknown as Record<string, unknown>,
    );
    vi.spyOn(client.endpoints, "list").mockResolvedValue({
      items: MOCK_ENDPOINTS,
      total: MOCK_ENDPOINTS.length,
    });
    vi.spyOn(client.webhooks, "list").mockResolvedValue({
      items: [],
      total: 0,
    });

    renderAdmin();

    await waitFor(() => {
      expect(
        screen.getByText("No API keys generated yet."),
      ).toBeInTheDocument();
    });
  });

  it("updates default endpoint selection", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("default-endpoint-select")).toBeInTheDocument();
    });

    await user.selectOptions(
      screen.getByTestId("default-endpoint-select"),
      "ep2",
    );

    await waitFor(() => {
      expect(client.admin.updateSettings).toHaveBeenCalledWith(
        expect.objectContaining({ default_endpoint_id: "ep2" }),
      );
    });
  });

  it("updates token budget values", async () => {
    mockAllApis();
    const user = userEvent.setup();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("budget-daily")).toBeInTheDocument();
    });

    const dailyInput = screen.getByTestId("budget-daily");
    await user.clear(dailyInput);
    await user.type(dailyInput, "50000");

    await waitFor(() => {
      expect(client.admin.updateSettings).toHaveBeenCalled();
    });
  });

  it("shows no webhooks configured message when empty", async () => {
    vi.spyOn(client.admin, "getSettings").mockResolvedValue(
      MOCK_SETTINGS as unknown as Record<string, unknown>,
    );
    vi.spyOn(client.admin, "getStats").mockResolvedValue(
      MOCK_STATS as unknown as Record<string, unknown>,
    );
    vi.spyOn(client.endpoints, "list").mockResolvedValue({
      items: MOCK_ENDPOINTS,
      total: MOCK_ENDPOINTS.length,
    });
    vi.spyOn(client.webhooks, "list").mockResolvedValue({
      items: [],
      total: 0,
    });

    renderAdmin();

    await waitFor(() => {
      expect(
        screen.getByText("No webhooks configured."),
      ).toBeInTheDocument();
    });
  });

  it("shows storage usage formatted correctly", async () => {
    mockAllApis();
    renderAdmin();

    await waitFor(() => {
      expect(screen.getByTestId("stat-storage-usage")).toHaveTextContent(
        "50.0 MB",
      );
    });
  });
});
