/**
 * Unit tests for the water-log auth layer.
 *
 * Two modules are exercised here:
 *
 *   - `src/actions/water-log/auth.ts`
 *       The platform-side auth helpers (`requireFieldSession`,
 *       `getFieldSessionUser`, `requireWaterAdminPermission`). These
 *       are used by `/water` (irrigator PIN) and `/admin/water-log/*`
 *       (Neon Auth + admin_users gate).
 *
 *   - `src/lib/water-admin-pin-auth.ts`
 *       The brand-side PIN-only auth for `/water/admin/*`. Cookie
 *       presence = authenticated, no DB session table, no Neon Auth.
 *
 * Both modules are tested for the property that they never call
 * `getSession()` from `@/lib/auth`. The static check at the top of
 * the file enforces this on the platform-side module; the
 * brand-side module has no `@/lib/auth` import at all by design.
 */
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── Hoisted state (visible inside vi.mock factory closures) ────────────

const state = vi.hoisted(() => ({
  cookieValue: undefined as string | undefined,
  mockDb: { select: vi.fn() } as { select: ReturnType<typeof vi.fn> },
  mockWithPlatformAdmin: null as unknown as ReturnType<typeof vi.fn>,
  mockWithBrand: null as unknown as ReturnType<typeof vi.fn>,
  mockGetAdminUser: null as unknown as ReturnType<typeof vi.fn>,
}));

state.mockWithPlatformAdmin = vi.fn(
  async (fn: (db: typeof state.mockDb) => Promise<unknown>) => fn(state.mockDb),
);
state.mockWithBrand = vi.fn(
  async (_brandId: string, fn: (db: typeof state.mockDb) => Promise<unknown>) =>
    fn(state.mockDb),
);
state.mockGetAdminUser = vi.fn();

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

vi.mock("@/db/client", () => ({
  withPlatformAdmin: state.mockWithPlatformAdmin,
  withBrand: state.mockWithBrand,
  withDb: vi.fn(),
}));

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

vi.mock("next/headers", () => ({
  cookies: () =>
    Promise.resolve({
      get: () =>
        state.cookieValue !== undefined
          ? { value: state.cookieValue }
          : undefined,
      set: vi.fn(),
      delete: vi.fn(),
    }),
  headers: () => Promise.resolve(new Headers()),
}));

beforeEach(() => {
  state.cookieValue = undefined;
  state.mockDb.select.mockReset();
  state.mockWithPlatformAdmin.mockClear();
  state.mockWithBrand.mockClear();
  state.mockGetAdminUser.mockReset();
});

// ── Module guards (the regression this whole PR exists for) ─────────────

describe("water-log/auth — module surface", () => {
  it("does not import getSession from @/lib/auth", async () => {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const raw = await fs.readFile(
      path.resolve(process.cwd(), "src/actions/water-log/auth.ts"),
      "utf8",
    );
    const stripped = raw
      .replace(/\/\*[\s\S]*?\*\//g, "")
      .replace(/^\s*\/\/.*$/gm, "")
      .replace(/\s+/g, " ");
    expect(stripped).not.toMatch(/from\s+["']@\/lib\/auth["']/);
    expect(stripped).not.toMatch(/\bgetSession\s*\(/);
  });
});

describe("water-admin-pin-auth — module surface", () => {
  it("does not import getSession from @/lib/auth", async () => {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const raw = await fs.readFile(
      path.resolve(process.cwd(), "src/lib/water-admin-pin-auth.ts"),
      "utf8",
    );
    const stripped = raw
      .replace(/\/\*[\s\S]*?\*\//g, "")
      .replace(/^\s*\/\/.*$/gm, "")
      .replace(/\s+/g, " ");
    expect(stripped).not.toMatch(/from\s+["']@\/lib\/auth["']/);
    expect(stripped).not.toMatch(/\bgetSession\s*\(/);
    expect(stripped).not.toMatch(/\bgetAdminUser\s*\(/);
  });
});

// ── requireFieldSession ─────────────────────────────────────────────────

describe("requireFieldSession()", () => {
  it("returns { ok: false, error: 'Not logged in' } when wl_session cookie is missing", async () => {
    state.cookieValue = undefined;
    const { requireFieldSession } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireFieldSession();
    expect(result).toEqual({ ok: false, error: "Not logged in" });
    expect(state.mockWithPlatformAdmin).not.toHaveBeenCalled();
  });

  it("returns { ok: false, error: 'Session not found' } when cookie has no matching row", async () => {
    state.cookieValue = "missing-session-id";
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        innerJoin: () => ({
          where: () => ({
            limit: async () => [],
          }),
        }),
      }),
    });
    const { requireFieldSession } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireFieldSession();
    expect(result).toEqual({ ok: false, error: "Session not found" });
  });

  it("returns { ok: false, error: 'Session expired' } and best-effort deletes the row when past expires_at", async () => {
    state.cookieValue = "expired-id";
    const past = new Date(Date.now() - 60_000);
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        innerJoin: () => ({
          where: () => ({
            limit: async () => [
              {
                session: { id: "expired-id", expiresAt: past },
                irrigator: {
                  id: "u1",
                  brandId: "b1",
                  role: "irrigator",
                  active: true,
                },
              },
            ],
          }),
        }),
      }),
    });
    const mockDelete = vi.fn().mockReturnValue({
      where: async () => undefined,
    });
    (state.mockDb as unknown as { select: typeof vi.fn; delete: typeof vi.fn }).delete =
      mockDelete;
    const { requireFieldSession } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireFieldSession();
    expect(result).toEqual({ ok: false, error: "Session expired" });
    expect(mockDelete).toHaveBeenCalled();
  });

  it("returns { ok: false, error: 'User is inactive' } when irrigator is soft-deleted", async () => {
    state.cookieValue = "valid-id";
    const future = new Date(Date.now() + 60_000);
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        innerJoin: () => ({
          where: () => ({
            limit: async () => [
              {
                session: { id: "valid-id", expiresAt: future },
                irrigator: {
                  id: "u1",
                  brandId: "b1",
                  role: "irrigator",
                  active: false,
                },
              },
            ],
          }),
        }),
      }),
    });
    const { requireFieldSession } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireFieldSession();
    expect(result).toEqual({ ok: false, error: "User is inactive" });
  });

  it("returns { ok: true, userId, brandId, role } on a happy-path session", async () => {
    state.cookieValue = "valid-id";
    const future = new Date(Date.now() + 60_000);
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        innerJoin: () => ({
          where: () => ({
            limit: async () => [
              {
                session: { id: "valid-id", expiresAt: future },
                irrigator: {
                  id: "u1",
                  brandId: "b1",
                  role: "irrigator",
                  active: true,
                },
              },
            ],
          }),
        }),
      }),
    });
    const { requireFieldSession } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireFieldSession();
    expect(result).toEqual({
      ok: true,
      userId: "u1",
      brandId: "b1",
      role: "irrigator",
    });
  });
});

// ── getFieldSessionUser ─────────────────────────────────────────────────

describe("getFieldSessionUser()", () => {
  it("returns null when there is no active field session", async () => {
    state.cookieValue = undefined;
    const { getFieldSessionUser } = await import(
      "@/actions/water-log/auth"
    );
    expect(await getFieldSessionUser()).toBeNull();
  });
});

// ── requireWaterAdminPermission (site-admin gate) ───────────────────────

describe("requireWaterAdminPermission()", () => {
  it("returns { ok: false, error: 'Not signed in' } when getAdminUser is null", async () => {
    state.mockGetAdminUser.mockResolvedValue(null);
    const { requireWaterAdminPermission } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireWaterAdminPermission();
    expect(result).toEqual({ ok: false, error: "Not signed in" });
  });

  it("returns { ok: false, error: 'Not authorized' } when admin lacks can_manage_water_log and is not platform_admin", async () => {
    state.mockGetAdminUser.mockResolvedValue({
      user_id: "u1",
      role: "brand_admin",
      can_manage_water_log: false,
    });
    const { requireWaterAdminPermission } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireWaterAdminPermission();
    expect(result).toEqual({ ok: false, error: "Not authorized" });
  });

  it("returns { ok: true, adminUser } when platform_admin", async () => {
    const adminUser = { user_id: "u1", role: "platform_admin" };
    state.mockGetAdminUser.mockResolvedValue(adminUser);
    const { requireWaterAdminPermission } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireWaterAdminPermission();
    expect(result).toEqual({ ok: true, adminUser });
  });

  it("returns { ok: true, adminUser } when brand_admin with can_manage_water_log", async () => {
    const adminUser = {
      user_id: "u1",
      role: "brand_admin",
      can_manage_water_log: true,
    };
    state.mockGetAdminUser.mockResolvedValue(adminUser);
    const { requireWaterAdminPermission } = await import(
      "@/actions/water-log/auth"
    );
    const result = await requireWaterAdminPermission();
    expect(result).toEqual({ ok: true, adminUser });
  });
});

// ── getAdminSession (cookie-only, the new simple auth) ──────────────────

describe("getAdminSession()", () => {
  it("returns null when wl_admin_session cookie is missing", async () => {
    state.cookieValue = undefined;
    const { getAdminSession } = await import(
      "@/lib/water-admin-pin-auth"
    );
    expect(await getAdminSession()).toBeNull();
  });

  it("returns the session shape on a valid cookie value", async () => {
    state.cookieValue = "abc-123";
    const { getAdminSession, TUXEDO_BRAND_ID } = await import(
      "@/lib/water-admin-pin-auth"
    );
    expect(await getAdminSession()).toEqual({
      sessionId: "abc-123",
      brandId: TUXEDO_BRAND_ID,
    });
  });
});

// ── verifyAdminPin (PIN verify + cookie set, no DB session) ─────────────

describe("verifyAdminPin()", () => {
  function makeSettingsRow(overrides: Partial<{
    enabled: boolean;
    pinHash: string | null;
    sessionDurationHours: number;
  }> = {}) {
    return {
      enabled: overrides.enabled ?? true,
      pinHash: overrides.pinHash ?? "scrypt$16384$8$1$AAAAAAAAAAAAAAAAAAAAAA==$BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
      sessionDurationHours: overrides.sessionDurationHours ?? 8,
    };
  }

  it("returns ok:false 'No PIN configured' when stored hash is missing", async () => {
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        where: () => ({
          limit: async () => [{ enabled: true, pinHash: null, sessionDurationHours: 8 }],
        }),
      }),
    });
    const { verifyAdminPin } = await import("@/lib/water-admin-pin-auth");
    const result = await verifyAdminPin("1234");
    expect(result).toEqual({
      ok: false,
      error: "No PIN configured — generate one in /admin/water-log/settings",
    });
  });

  it("returns ok:false 'Admin portal is disabled' when settings.enabled is false", async () => {
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        where: () => ({
          limit: async () => [{ enabled: false, pinHash: "any", sessionDurationHours: 8 }],
        }),
      }),
    });
    const { verifyAdminPin } = await import("@/lib/water-admin-pin-auth");
    const result = await verifyAdminPin("1234");
    expect(result).toEqual({ ok: false, error: "Admin portal is disabled" });
  });

  it("returns ok:false 'Invalid PIN' on PIN format failure (no DB read)", async () => {
    const { verifyAdminPin } = await import("@/lib/water-admin-pin-auth");
    const result = await verifyAdminPin("12ab");
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toMatch(/PIN/);
    }
    expect(state.mockWithBrand).not.toHaveBeenCalled();
  });

  it("returns ok:true when PIN matches the stored hash", async () => {
    // Pre-compute a real scrypt hash for "1234" so verifyPin succeeds.
    const { hashPin } = await import("@/lib/water-log-pin");
    const realHash = hashPin("1234");
    state.mockDb.select.mockReturnValueOnce({
      from: () => ({
        where: () => ({
          limit: async () => [
            makeSettingsRow({ pinHash: realHash, sessionDurationHours: 4 }),
          ],
        }),
      }),
    });
    const { verifyAdminPin, TUXEDO_BRAND_ID } = await import(
      "@/lib/water-admin-pin-auth"
    );
    const result = await verifyAdminPin("1234");
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.session.brandId).toBe(TUXEDO_BRAND_ID);
      expect(typeof result.session.sessionId).toBe("string");
      expect(result.session.sessionId.length).toBeGreaterThan(8);
    }
  });
});

// ── clearAdminSession ───────────────────────────────────────────────────

describe("clearAdminSession()", () => {
  it("returns without throwing", async () => {
    const { clearAdminSession } = await import(
      "@/lib/water-admin-pin-auth"
    );
    await expect(clearAdminSession()).resolves.toBeUndefined();
  });
});