import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("~/models/user", () => ({
  findUserById: vi.fn(),
  getUserDisplayName: vi.fn(),
}));

vi.mock("~/database/context", () => ({
  database: vi.fn(),
}));

import {
  createAuditLogEntry,
  getAuditLogForSeason,
  logCommissionerAction,
} from "../audit-log";
import { findUserById, getUserDisplayName } from "~/models/user";
import { database } from "~/database/context";

const SEASON_ID = "season-1";
const LEAGUE_ID = "league-1";
const ACTOR_USER_ID = "user-uuid-1";

const SAMPLE_ENTRY = {
  id: "entry-1",
  seasonId: SEASON_ID,
  leagueId: LEAGUE_ID,
  actorUserId: ACTOR_USER_ID,
  actorDisplayName: "Alice",
  action: "draft_rollback" as const,
  affectedTeamIds: ["team-1"],
  details: { rolledBackToPickNumber: 5, previousPickNumber: 10 },
  createdAt: new Date("2025-01-01T12:00:00Z"),
};

/**
 * Builds a mock db for insert operations.
 */
function makeInsertDb(returnValue: object) {
  return {
    insert: vi.fn().mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([returnValue]),
      }),
    }),
  };
}

/**
 * Builds a mock db for getAuditLogForSeason which fires two select queries in
 * parallel: one for the data rows and one for the count. We use two
 * mockResolvedValueOnce calls so the first promise.all resolves the data and
 * the second resolves the count.
 *
 * Both calls share the same chain: select().from().where() — after that they
 * diverge (data adds .orderBy().limit().offset(), count stops). We build a
 * single chainable mock where the terminal call is a jest.fn that returns
 * different values on each invocation.
 */
function makeSelectDb(
  dataRows: object[],
  countRow: { count: number }
): object {
  // Count chain: select().from().where() → resolves to [countRow]
  const countChain = {
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([countRow]),
  };

  // Data chain: select().from().where().orderBy().limit().offset() → resolves to dataRows
  const dataChain = {
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockReturnThis(),
    orderBy: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    offset: vi.fn().mockResolvedValue(dataRows),
  };

  let selectCallCount = 0;
  return {
    select: vi.fn().mockImplementation(() => {
      // First call is the data query; second call is the count query
      selectCallCount++;
      return selectCallCount === 1 ? dataChain : countChain;
    }),
  };
}

beforeEach(() => {
  vi.clearAllMocks();
});

// ─── createAuditLogEntry ──────────────────────────────────────────────────────

describe("createAuditLogEntry", () => {
  it("inserts the entry and returns the persisted record", async () => {
    vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_ENTRY) as never);

    const result = await createAuditLogEntry({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      actorDisplayName: "Alice",
      action: "draft_rollback",
      affectedTeamIds: ["team-1"],
      details: { rolledBackToPickNumber: 5, previousPickNumber: 10 },
    });

    expect(result).toEqual(SAMPLE_ENTRY);
  });
});

// ─── getAuditLogForSeason ─────────────────────────────────────────────────────

describe("getAuditLogForSeason", () => {
  it("returns entries and computes hasMore=false when on the last page", async () => {
    vi.mocked(database).mockReturnValue(
      makeSelectDb([SAMPLE_ENTRY], { count: 1 }) as never
    );

    const result = await getAuditLogForSeason(SEASON_ID, {
      limit: 50,
      offset: 0,
    });

    expect(result.entries).toHaveLength(1);
    expect(result.total).toBe(1);
    expect(result.hasMore).toBe(false);
  });

  it("returns hasMore=true when more entries remain", async () => {
    // 51 total, fetching first 50 starting at offset 0 → 50 returned → hasMore
    const manyEntries = Array.from({ length: 50 }, (_, i) => ({
      ...SAMPLE_ENTRY,
      id: `entry-${i}`,
    }));

    vi.mocked(database).mockReturnValue(
      makeSelectDb(manyEntries, { count: 51 }) as never
    );

    const result = await getAuditLogForSeason(SEASON_ID, {
      limit: 50,
      offset: 0,
    });

    expect(result.total).toBe(51);
    expect(result.hasMore).toBe(true);
  });

  it("returns hasMore=false on the final page", async () => {
    // 51 total, on page 2 (offset=50), 1 entry returned
    vi.mocked(database).mockReturnValue(
      makeSelectDb([SAMPLE_ENTRY], { count: 51 }) as never
    );

    const result = await getAuditLogForSeason(SEASON_ID, {
      limit: 50,
      offset: 50,
    });

    expect(result.hasMore).toBe(false);
  });

  it("uses default limit of 50 and offset of 0 when options are omitted", async () => {
    const db = makeSelectDb([SAMPLE_ENTRY], { count: 1 });
    vi.mocked(database).mockReturnValue(db as never);

    await getAuditLogForSeason(SEASON_ID);

    // Verify the limit and offset were applied via the data chain
    const dataChain = (db as { select: ReturnType<typeof vi.fn> }).select.mock.results[0].value;
    expect(dataChain.limit).toHaveBeenCalledWith(50);
    expect(dataChain.offset).toHaveBeenCalledWith(0);
  });

  it("returns an empty page when no entries exist", async () => {
    vi.mocked(database).mockReturnValue(
      makeSelectDb([], { count: 0 }) as never
    );

    const result = await getAuditLogForSeason(SEASON_ID);

    expect(result.entries).toHaveLength(0);
    expect(result.total).toBe(0);
    expect(result.hasMore).toBe(false);
  });
});

// ─── logCommissionerAction ────────────────────────────────────────────────────

describe("logCommissionerAction", () => {
  it("resolves the actor display name from the users table", async () => {
    const mockUser = { username: "alice", displayName: "Alice Smith" };
    vi.mocked(findUserById).mockResolvedValue(mockUser as never);
    vi.mocked(getUserDisplayName).mockReturnValue("alice");

    const insertDb = makeInsertDb(SAMPLE_ENTRY);
    vi.mocked(database).mockReturnValue(insertDb as never);

    await logCommissionerAction({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      action: "draft_rollback",
    });

    const insertValues = (insertDb.insert as ReturnType<typeof vi.fn>).mock.results[0].value.values.mock.calls[0][0];
    expect(insertValues.actorDisplayName).toBe("alice");
  });

  it("falls back to actorUserId if the user is not found", async () => {
    vi.mocked(findUserById).mockResolvedValue(undefined);
    vi.mocked(getUserDisplayName).mockReturnValue(null);

    const insertDb = makeInsertDb(SAMPLE_ENTRY);
    vi.mocked(database).mockReturnValue(insertDb as never);

    await logCommissionerAction({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      action: "draft_rollback",
    });

    const insertValues = (insertDb.insert as ReturnType<typeof vi.fn>).mock.results[0].value.values.mock.calls[0][0];
    expect(insertValues.actorDisplayName).toBe(ACTOR_USER_ID);
  });

  it("defaults affectedTeamIds to [] when not provided", async () => {
    vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never);
    vi.mocked(getUserDisplayName).mockReturnValue("alice");

    const insertDb = makeInsertDb(SAMPLE_ENTRY);
    vi.mocked(database).mockReturnValue(insertDb as never);

    await logCommissionerAction({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      action: "draft_started",
    });

    const insertValues = (insertDb.insert as ReturnType<typeof vi.fn>).mock.results[0].value.values.mock.calls[0][0];
    expect(insertValues.affectedTeamIds).toEqual([]);
  });

  it("defaults details to {} when not provided", async () => {
    vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never);
    vi.mocked(getUserDisplayName).mockReturnValue("alice");

    const insertDb = makeInsertDb(SAMPLE_ENTRY);
    vi.mocked(database).mockReturnValue(insertDb as never);

    await logCommissionerAction({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      action: "draft_started",
    });

    const insertValues = (insertDb.insert as ReturnType<typeof vi.fn>).mock.results[0].value.values.mock.calls[0][0];
    expect(insertValues.details).toEqual({});
  });

  it("passes through provided affectedTeamIds and details", async () => {
    vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never);
    vi.mocked(getUserDisplayName).mockReturnValue("alice");

    const insertDb = makeInsertDb(SAMPLE_ENTRY);
    vi.mocked(database).mockReturnValue(insertDb as never);

    const details = { pickNumber: 7, teamName: "Team Bravo", participantName: "Max V" };
    await logCommissionerAction({
      seasonId: SEASON_ID,
      leagueId: LEAGUE_ID,
      actorUserId: ACTOR_USER_ID,
      action: "force_manual_pick",
      affectedTeamIds: ["team-7"],
      details,
    });

    const insertValues = (insertDb.insert as ReturnType<typeof vi.fn>).mock.results[0].value.values.mock.calls[0][0];
    expect(insertValues.affectedTeamIds).toEqual(["team-7"]);
    expect(insertValues.details).toEqual(details);
  });
});
