/**
 * Unit tests for `createAdminUser` in `src/actions/admin/users.ts`.
 *
 * The action does three things, in order:
 *   1. Authorize the caller (platform_admin only) — tested via
 *      `getAdminUser` mock.
 *   2. Create a Neon Auth account (via `auth.admin.createUser` first,
 *      with a public sign-up fallback).
 *   3. Insert the local `admin_users` row + brand link inside a tx,
 *      send a welcome email (best-effort), and return the result.
 *
 * These tests mock the Neon Auth SDK, the DB layer, and the email
 * service so the orchestration logic can be exercised without a real
 * network or DB.
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// Use vi.hoisted to make mock fns available inside the hoisted
// vi.mock factories.
const {
  mockGetAdminUser,
  mockNeonAuthCreateUser,
  mockQuery,
  mockWithTx,
  mockSendWelcomeEmail,
  mockGetBrandSettings,
} = vi.hoisted(() => ({
  mockGetAdminUser: vi.fn(),
  mockNeonAuthCreateUser: vi.fn(),
  mockQuery: vi.fn(),
  mockWithTx: vi.fn(),
  mockSendWelcomeEmail: vi.fn(),
  mockGetBrandSettings: vi.fn(),
}));

vi.mock("server-only", () => ({}));

vi.mock("@/lib/admin-permissions", () => ({
  getAdminUser: mockGetAdminUser,
}));

vi.mock("@/lib/auth", () => ({
  createUser: mockNeonAuthCreateUser,
}));

vi.mock("@/lib/db", () => ({
  query: mockQuery,
  withTx: mockWithTx,
}));

vi.mock("@/lib/email-service", () => ({
  sendWelcomeEmail: mockSendWelcomeEmail,
}));

vi.mock("@/actions/brand-settings", () => ({
  getBrandSettings: mockGetBrandSettings,
}));

vi.mock("next/headers", () => ({
  cookies: () => Promise.resolve({ get: () => undefined }),
}));

// `fetch` is used by the signup fallback. Default to a harmless
// 200 — tests that exercise the fallback override this.
const originalFetch = global.fetch;

import { createAdminUser } from "@/actions/admin/users";

const PLATFORM_ADMIN = {
  id: "platform-1",
  email: "root@example.com",
  role: "platform_admin" as const,
  brand_id: null,
  brand_slug: null,
  brand_ids: [],
  display_name: "Root",
  active: true,
  auth_provider: "neon_auth" as const,
  can_manage_products: true,
  can_manage_stops: true,
  can_manage_orders: true,
  can_manage_pickup: true,
  can_manage_messages: true,
  can_manage_refunds: true,
  can_manage_users: true,
  can_manage_water_log: true,
  can_manage_reports: true,
  can_manage_settings: true,
  can_manage_billing: true,
  can_manage_branding: true,
  can_manage_marketing: true,
  can_manage_team: true,
  must_change_password: false,
  user_id: "platform-1",
};

const BRAND_ADMIN = { ...PLATFORM_ADMIN, role: "brand_admin" as const, brand_id: "brand-tux" };

const baseInput = {
  email: "newuser@example.com",
  password: "TempPass123!",
  role: "brand_admin" as const,
  brand_id: "brand-tux",
  display_name: "New User",
  flags: { can_manage_orders: true },
  mustChangePassword: true,
};

beforeEach(() => {
  vi.clearAllMocks();
  // Default: platform admin caller, no-op fetch, happy email path.
  mockGetAdminUser.mockResolvedValue(PLATFORM_ADMIN);
  mockSendWelcomeEmail.mockResolvedValue({ ok: true });
  mockGetBrandSettings.mockResolvedValue({
    success: true,
    settings: { brand_name: "Tuxedo Citrus", logo_url: null },
  });
  // Set env vars the action reads at call time.
  process.env.NEON_AUTH_BASE_URL = "https://test.neonauth.example/auth";
  process.env.NEXT_PUBLIC_SITE_URL = "http://localhost:4000";
  global.fetch = vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    json: async () => ({ user: { id: "neon-user-from-signup" } }),
  } as Response);
});

afterEach(() => {
  global.fetch = originalFetch;
});

describe("createAdminUser — authorization", () => {
  it("rejects unauthenticated callers", async () => {
    mockGetAdminUser.mockResolvedValue(null);
    const r = await createAdminUser(baseInput);
    expect(r.user).toBeNull();
    expect(r.error).toMatch(/not authenticated/i);
    expect(mockNeonAuthCreateUser).not.toHaveBeenCalled();
    expect(mockWithTx).not.toHaveBeenCalled();
  });

  it("rejects non-platform-admin callers", async () => {
    mockGetAdminUser.mockResolvedValue(BRAND_ADMIN);
    const r = await createAdminUser(baseInput);
    expect(r.user).toBeNull();
    expect(r.error).toMatch(/only platform admins/i);
    expect(mockNeonAuthCreateUser).not.toHaveBeenCalled();
  });
});

describe("createAdminUser — happy path via auth.admin.createUser", () => {
  it("uses the privileged admin endpoint when it succeeds", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: { user: { id: "neon-user-abc" } },
      error: null,
    });
    mockWithTx.mockImplementation(async (fn) => {
      // Simulate the tx client: record what the action called, then
      // return a fresh admin row.
      return fn({
        query: vi
          .fn()
          .mockResolvedValueOnce({
            rows: [
              {
                id: "admin-row-1",
                user_id: "neon-user-abc",
                display_name: "New User",
                email: "newuser@example.com",
                phone_number: null,
                role: "brand_admin",
                brand_id: "brand-tux",
                can_manage_products: false,
                can_manage_stops: false,
                can_manage_orders: true,
                can_manage_pickup: false,
                can_manage_messages: false,
                can_manage_refunds: false,
                can_manage_users: false,
                can_manage_water_log: false,
                can_manage_reports: false,
                active: true,
                must_change_password: true,
                created_at: "2026-06-17T00:00:00Z",
                last_login: null,
              },
            ],
          })
          // Second query inside the tx: admin_user_brands link.
          .mockResolvedValueOnce({ rows: [] }),
      } as never);
    });

    const r = await createAdminUser(baseInput);
    expect(r.error).toBeNull();
    expect(r.user?.email).toBe("newuser@example.com");
    expect(r.tempPassword).toBe(baseInput.password);
    expect(r.emailSent).toBe(true);
    expect(r.authPath).toBe("admin");
    expect(mockNeonAuthCreateUser).toHaveBeenCalledWith({
      email: "newuser@example.com",
      password: "TempPass123!",
      name: "New User",
    });
    // Fallback should NOT have been called.
    expect(global.fetch).not.toHaveBeenCalled();
  });
});

describe("createAdminUser — fallback to public sign-up", () => {
  it("falls back to /sign-up/email when the admin endpoint fails with FORBIDDEN", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: null,
      error: { code: "FORBIDDEN", message: "Admin role required" },
    });
    mockWithTx.mockImplementation(async (fn) =>
      fn({
        query: vi
          .fn()
          .mockResolvedValueOnce({
            rows: [
              {
                id: "admin-row-2",
                user_id: "neon-user-from-signup",
                display_name: "New User",
                email: "newuser@example.com",
                phone_number: null,
                role: "brand_admin",
                brand_id: "brand-tux",
                can_manage_products: false,
                can_manage_stops: false,
                can_manage_orders: true,
                can_manage_pickup: false,
                can_manage_messages: false,
                can_manage_refunds: false,
                can_manage_users: false,
                can_manage_water_log: false,
                can_manage_reports: false,
                active: true,
                must_change_password: true,
                created_at: "2026-06-17T00:00:00Z",
                last_login: null,
              },
            ],
          })
          .mockResolvedValueOnce({ rows: [] }),
      } as never),
    );
    // The fallback calls `query` to flip emailVerified=true. Pre-set
    // a default that satisfies that call (and any future ones).
    mockQuery.mockResolvedValue({ rows: [] });

    const r = await createAdminUser(baseInput);
    expect(r.error).toBeNull();
    expect(r.authPath).toBe("signup");
    expect(r.user?.user_id).toBe("neon-user-from-signup");
    expect(global.fetch).toHaveBeenCalledTimes(1);
    const [url, init] = (global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(url).toMatch(/\/sign-up\/email$/);
    expect(JSON.parse(init.body)).toEqual({
      email: "newuser@example.com",
      password: "TempPass123!",
      name: "New User",
      callbackURL: "/admin",
    });
  });

  it("rejects with a clear error when the email is already in use", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: null,
      error: {
        code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
        message: "User already exists",
      },
    });
    const r = await createAdminUser(baseInput);
    expect(r.user).toBeNull();
    expect(r.error).toMatch(/already exists/i);
    // We should not have called the fallback or the DB.
    expect(global.fetch).not.toHaveBeenCalled();
    expect(mockWithTx).not.toHaveBeenCalled();
  });

  it("surfaces the fallback error if the public sign-up fails", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: null,
      error: { code: "INTERNAL_SERVER_ERROR", message: "boom" },
    });
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 500,
      json: async () => ({ code: "INTERNAL_SERVER_ERROR", message: "sign-up down" }),
    } as Response);

    const r = await createAdminUser(baseInput);
    expect(r.user).toBeNull();
    expect(r.error).toMatch(/sign-up down/i);
    expect(mockWithTx).not.toHaveBeenCalled();
  });
});

describe("createAdminUser — DB failure after auth success", () => {
  it("returns an error explaining the orphaned Neon Auth user", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: { user: { id: "neon-user-orphaned" } },
      error: null,
    });
    mockWithTx.mockRejectedValue(new Error("FK violation on brand_id"));

    const r = await createAdminUser(baseInput);
    expect(r.user).toBeNull();
    expect(r.error).toMatch(/orphaned/i);
    expect(r.error).toMatch(/FK violation/);
  });
});

describe("createAdminUser — email is best-effort", () => {
  it("still returns success when the welcome email fails", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: { user: { id: "neon-user-abc" } },
      error: null,
    });
    mockWithTx.mockImplementation(async (fn) =>
      fn({
        query: vi.fn().mockResolvedValueOnce({
          rows: [
            {
              id: "admin-row-3",
              user_id: "neon-user-abc",
              display_name: "New User",
              email: "newuser@example.com",
              phone_number: null,
              role: "brand_admin",
              brand_id: "brand-tux",
              can_manage_products: false,
              can_manage_stops: false,
              can_manage_orders: true,
              can_manage_pickup: false,
              can_manage_messages: false,
              can_manage_refunds: false,
              can_manage_users: false,
              can_manage_water_log: false,
              can_manage_reports: false,
              active: true,
              must_change_password: true,
              created_at: "2026-06-17T00:00:00Z",
              last_login: null,
            },
          ],
        }),
      } as never),
    );
    mockSendWelcomeEmail.mockResolvedValue({ ok: false, error: "Resend 422: domain not verified" });

    const r = await createAdminUser(baseInput);
    expect(r.error).toBeNull();
    expect(r.user).not.toBeNull();
    expect(r.emailSent).toBe(false);
    expect(r.emailError).toBe("Resend 422: domain not verified");
    expect(r.tempPassword).toBe(baseInput.password);
  });

  it("records the email error message when sendWelcomeEmail throws", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: { user: { id: "neon-user-abc" } },
      error: null,
    });
    mockWithTx.mockImplementation(async (fn) =>
      fn({
        query: vi.fn().mockResolvedValueOnce({
          rows: [
            {
              id: "admin-row-4",
              user_id: "neon-user-abc",
              display_name: "New User",
              email: "newuser@example.com",
              phone_number: null,
              role: "brand_admin",
              brand_id: "brand-tux",
              can_manage_products: false,
              can_manage_stops: false,
              can_manage_orders: true,
              can_manage_pickup: false,
              can_manage_messages: false,
              can_manage_refunds: false,
              can_manage_users: false,
              can_manage_water_log: false,
              can_manage_reports: false,
              active: true,
              must_change_password: true,
              created_at: "2026-06-17T00:00:00Z",
              last_login: null,
            },
          ],
        }),
      } as never),
    );
    mockSendWelcomeEmail.mockRejectedValue(new Error("Resend 401"));

    const r = await createAdminUser(baseInput);
    expect(r.error).toBeNull();
    expect(r.emailSent).toBe(false);
    expect(r.emailError).toMatch(/Resend 401/);
  });
});

describe("createAdminUser — no brand", () => {
  it("creates a platform admin without inserting an admin_user_brands link", async () => {
    mockNeonAuthCreateUser.mockResolvedValue({
      data: { user: { id: "neon-user-platform" } },
      error: null,
    });
    const txClient = {
      query: vi.fn().mockResolvedValueOnce({
        rows: [
          {
            id: "admin-row-platform",
            user_id: "neon-user-platform",
            display_name: "New Platform",
            email: "platform2@example.com",
            phone_number: null,
            role: "platform_admin",
            brand_id: null,
            can_manage_products: true,
            can_manage_stops: true,
            can_manage_orders: true,
            can_manage_pickup: true,
            can_manage_messages: true,
            can_manage_refunds: true,
            can_manage_users: true,
            can_manage_water_log: true,
            can_manage_reports: true,
            active: true,
            must_change_password: true,
            created_at: "2026-06-17T00:00:00Z",
            last_login: null,
          },
        ],
      }),
    };
    mockWithTx.mockImplementation(async (fn) => fn(txClient as never));

    const r = await createAdminUser({
      ...baseInput,
      email: "platform2@example.com",
      role: "platform_admin",
      brand_id: null,
    });
    expect(r.error).toBeNull();
    expect(r.user?.role).toBe("platform_admin");
    // Only one query inside the tx: the INSERT. No brand link INSERT.
    expect(txClient.query).toHaveBeenCalledTimes(1);
  });
});
