/**
 * Unit tests for GET /api/cron/auth-anomaly-check.
 *
 * Tests cover:
 *   - CRON_SECRET enforcement (fail-closed, timing-safe)
 *   - No anomalies below threshold
 *   - HIGH_GLOBAL_FAILURE_RATE signal when global threshold breached
 *   - CONCENTRATED_FAILURES signal when per-entity threshold breached
 *   - Notifications sent to admins only when anomalies exist
 *   - No admin notification when no anomalies
 */

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { THRESHOLDS } from "./route.js";

// ─── Prisma mock ─────────────────────────────────────────────────────────────
const auditLogFindManyMock = vi.hoisted(() => vi.fn());
const userFindManyMock = vi.hoisted(() => vi.fn());

vi.mock("@capakraken/db", () => ({
  prisma: {
    auditLog: { findMany: auditLogFindManyMock },
    user: { findMany: userFindManyMock },
  },
}));

// ─── createNotificationsForUsers mock ─────────────────────────────────────────
const createNotificationsMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));

vi.mock("@capakraken/api", () => ({
  createNotificationsForUsers: createNotificationsMock,
}));

vi.mock("@capakraken/api/lib/logger", () => ({
  logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() },
}));

// ─── cron-auth mock ───────────────────────────────────────────────────────────
const verifyCronSecretMock = vi.hoisted(() => vi.fn());

vi.mock("~/lib/cron-auth.js", () => ({
  verifyCronSecret: verifyCronSecretMock,
}));

// ─── helpers ──────────────────────────────────────────────────────────────────

function makeFailureEvent(entityId = "user_1") {
  return { entityId, summary: "Login failed — invalid password" };
}

function makeRequest() {
  return new Request("http://localhost/api/cron/auth-anomaly-check", {
    headers: { Authorization: "Bearer test-secret" },
  });
}

// ─── import after mocks ───────────────────────────────────────────────────────
// Route is imported lazily via dynamic import inside tests so that
// vi.mock() hoisting is guaranteed to complete first.
async function importRoute() {
  const mod = await import("./route.js");
  return mod;
}

// ─── tests ────────────────────────────────────────────────────────────────────

describe("GET /api/cron/auth-anomaly-check — cron secret enforcement", () => {
  beforeEach(() => { vi.clearAllMocks(); });

  it("returns 401 when verifyCronSecret denies the request", async () => {
    const { NextResponse } = await import("next/server");
    verifyCronSecretMock.mockReturnValue(
      NextResponse.json({ error: "Unauthorized" }, { status: 401 }),
    );
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    expect(res.status).toBe(401);
  });

  it("proceeds when verifyCronSecret returns null (allowed)", async () => {
    verifyCronSecretMock.mockReturnValue(null);
    auditLogFindManyMock.mockResolvedValue([]);
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    expect(res.status).toBe(200);
  });
});

describe("GET /api/cron/auth-anomaly-check — no anomalies", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    verifyCronSecretMock.mockReturnValue(null);
  });

  it("returns ok:true with zero anomalies when failures are below thresholds", async () => {
    // Spread failures across different entities so neither the global nor the
    // per-entity threshold is breached.
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.perEntityFailures - 1 }, (_, i) =>
        makeFailureEvent(`user_${i}`),
      ),
    );
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    const body = await res.json() as { ok: boolean; anomalies: unknown[] };
    expect(res.status).toBe(200);
    expect(body.ok).toBe(true);
    expect(body.anomalies).toHaveLength(0);
  });

  it("does not notify admins when no anomalies are found", async () => {
    auditLogFindManyMock.mockResolvedValue([]);
    const { GET } = await importRoute();
    await GET(makeRequest());
    expect(createNotificationsMock).not.toHaveBeenCalled();
  });
});

describe("GET /api/cron/auth-anomaly-check — HIGH_GLOBAL_FAILURE_RATE", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    verifyCronSecretMock.mockReturnValue(null);
  });

  it("detects HIGH_GLOBAL_FAILURE_RATE when total failures reach the threshold", async () => {
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.globalFailures }, () => makeFailureEvent("user_x")),
    );
    userFindManyMock.mockResolvedValue([{ id: "admin_1" }]);
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    const body = await res.json() as { anomalies: Array<{ type: string }> };
    expect(body.anomalies.some((a) => a.type === "HIGH_GLOBAL_FAILURE_RATE")).toBe(true);
  });

  it("notifies admins when HIGH_GLOBAL_FAILURE_RATE is detected", async () => {
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.globalFailures }, () => makeFailureEvent("user_x")),
    );
    userFindManyMock.mockResolvedValue([{ id: "admin_1" }]);
    const { GET } = await importRoute();
    await GET(makeRequest());
    expect(createNotificationsMock).toHaveBeenCalledOnce();
    const call = createNotificationsMock.mock.calls[0]![0] as { userIds: string[]; priority: string };
    expect(call.userIds).toContain("admin_1");
    expect(call.priority).toBe("CRITICAL");
  });
});

describe("GET /api/cron/auth-anomaly-check — CONCENTRATED_FAILURES", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    verifyCronSecretMock.mockReturnValue(null);
  });

  it("detects CONCENTRATED_FAILURES when per-entity failures reach the threshold", async () => {
    // One entity hits the per-entity threshold (but total stays below global)
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.perEntityFailures }, () => makeFailureEvent("target_user")),
    );
    userFindManyMock.mockResolvedValue([{ id: "admin_1" }]);
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    const body = await res.json() as { anomalies: Array<{ type: string; entityId: string }> };
    const concentrated = body.anomalies.find((a) => a.type === "CONCENTRATED_FAILURES");
    expect(concentrated).toBeDefined();
    expect(concentrated!.entityId).toBe("target_user");
  });

  it("does not flag an entity that is below the per-entity threshold", async () => {
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.perEntityFailures - 1 }, () => makeFailureEvent("target_user")),
    );
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    const body = await res.json() as { anomalies: Array<{ type: string }> };
    expect(body.anomalies.some((a) => a.type === "CONCENTRATED_FAILURES")).toBe(false);
  });

  it("does not notify when no admins exist", async () => {
    auditLogFindManyMock.mockResolvedValue(
      Array.from({ length: THRESHOLDS.perEntityFailures }, () => makeFailureEvent("target_user")),
    );
    userFindManyMock.mockResolvedValue([]); // no admins
    const { GET } = await importRoute();
    await GET(makeRequest());
    expect(createNotificationsMock).not.toHaveBeenCalled();
  });
});

describe("GET /api/cron/auth-anomaly-check — error handling", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    verifyCronSecretMock.mockReturnValue(null);
  });

  it("returns 500 when the DB query throws", async () => {
    auditLogFindManyMock.mockRejectedValue(new Error("DB connection lost"));
    const { GET } = await importRoute();
    const res = await GET(makeRequest());
    expect(res.status).toBe(500);
    const body = await res.json() as { ok: boolean };
    expect(body.ok).toBe(false);
  });
});

// ─── Unit tests for detectAuthAnomalies() ─────────────────────────────────────
// These call the exported pure function directly, bypassing the HTTP layer and
// the CRON_SECRET check, to verify threshold logic in isolation.

describe("detectAuthAnomalies — unit tests", () => {
  beforeEach(() => { vi.clearAllMocks(); });

  it("returns empty anomalies and zero totalFailures when no events are found", async () => {
    auditLogFindManyMock.mockResolvedValue([]);
    const { detectAuthAnomalies } = await importRoute();

    const result = await detectAuthAnomalies();

    expect(result.totalFailures).toBe(0);
    expect(result.anomalies).toHaveLength(0);
  });

  it("returns HIGH_GLOBAL_FAILURE_RATE when total failures reach the global threshold", async () => {
    const events = Array.from({ length: THRESHOLDS.globalFailures }, (_, i) => ({
      entityId: `user_${i}`,
      summary: "Login failed: bad password",
    }));
    auditLogFindManyMock.mockResolvedValue(events);
    const { detectAuthAnomalies } = await importRoute();

    const result = await detectAuthAnomalies();

    const globalAnomaly = result.anomalies.find(
      (a) => a.type === "HIGH_GLOBAL_FAILURE_RATE",
    );
    expect(globalAnomaly).toBeDefined();
    expect(globalAnomaly!.count).toBe(THRESHOLDS.globalFailures);
    expect(result.totalFailures).toBe(THRESHOLDS.globalFailures);
  });

  it("returns CONCENTRATED_FAILURES when one entityId accumulates enough failures", async () => {
    const events = Array.from({ length: THRESHOLDS.perEntityFailures }, () => ({
      entityId: "user_attacker",
      summary: "Login failed: bad password",
    }));
    auditLogFindManyMock.mockResolvedValue(events);
    const { detectAuthAnomalies } = await importRoute();

    const result = await detectAuthAnomalies();

    const concentrated = result.anomalies.find(
      (a) => a.type === "CONCENTRATED_FAILURES",
    );
    expect(concentrated).toBeDefined();
    expect(concentrated!.count).toBe(THRESHOLDS.perEntityFailures);
    expect(concentrated!.entityId).toBe("user_attacker");
  });

  it("does not flag CONCENTRATED_FAILURES when entityId is null, even when global threshold is reached", async () => {
    // 15 events all with null entityId — below global threshold (20) but we
    // explicitly confirm no CONCENTRATED_FAILURES regardless of count.
    const events = Array.from({ length: 15 }, () => ({
      entityId: null,
      summary: "Login failed: unknown user",
    }));
    auditLogFindManyMock.mockResolvedValue(events);
    const { detectAuthAnomalies } = await importRoute();

    const result = await detectAuthAnomalies();

    const concentrated = result.anomalies.find(
      (a) => a.type === "CONCENTRATED_FAILURES",
    );
    expect(concentrated).toBeUndefined();
  });

  it("fires both HIGH_GLOBAL_FAILURE_RATE and CONCENTRATED_FAILURES simultaneously", async () => {
    // 20 events total; 10 attributed to user_bot → both thresholds are hit.
    const botEvents = Array.from({ length: THRESHOLDS.perEntityFailures }, () => ({
      entityId: "user_bot",
      summary: "Login failed: bad password",
    }));
    const otherEvents = Array.from(
      { length: THRESHOLDS.globalFailures - THRESHOLDS.perEntityFailures },
      (_, i) => ({
        entityId: `user_other_${i}`,
        summary: "Login failed: bad password",
      }),
    );
    auditLogFindManyMock.mockResolvedValue([...botEvents, ...otherEvents]);
    const { detectAuthAnomalies } = await importRoute();

    const result = await detectAuthAnomalies();

    expect(result.totalFailures).toBe(THRESHOLDS.globalFailures);

    const globalAnomaly = result.anomalies.find(
      (a) => a.type === "HIGH_GLOBAL_FAILURE_RATE",
    );
    expect(globalAnomaly).toBeDefined();

    const concentrated = result.anomalies.find(
      (a) => a.type === "CONCENTRATED_FAILURES" && a.entityId === "user_bot",
    );
    expect(concentrated).toBeDefined();
    expect(concentrated!.count).toBe(THRESHOLDS.perEntityFailures);
  });
});
