import { Test } from '@nestjs/testing';
import { PrismaService } from 'nestjs-prisma';
import { AdminUsersReader } from './admin-users-reader.service';

interface MockPrisma {
  userDirectoryEntry: {
    count: jest.Mock;
    findMany: jest.Mock;
  };
  $transaction: jest.Mock;
}

function buildPrisma(opts?: { count?: number; items?: unknown[] }): MockPrisma {
  const count = jest.fn().mockResolvedValue(opts?.count ?? 0);
  const findMany = jest.fn().mockResolvedValue(opts?.items ?? []);
  return {
    userDirectoryEntry: { count, findMany },
    // The reader calls $transaction with the promises returned by
    // `count()` + `findMany()` — the operations have already fired
    // by the time $transaction sees them. The mock just resolves
    // them together so Prisma's tuple-return contract is honoured.
    $transaction: jest
      .fn()
      .mockImplementation((promises: ReadonlyArray<Promise<unknown>>) => Promise.all(promises)),
  };
}

async function createSubject(prisma: MockPrisma): Promise<AdminUsersReader> {
  const moduleRef = await Test.createTestingModule({
    providers: [AdminUsersReader, { provide: PrismaService, useValue: prisma }],
  }).compile();
  return moduleRef.get(AdminUsersReader);
}

const ROW = {
  oid: 'user-oid',
  tid: 'tenant-1',
  audience: 'workforce',
  username: 'jane.doe@apf.example',
  displayName: 'Jane Doe',
  firstSeenAt: new Date('2026-05-10T10:00:00.000Z'),
  lastSeenAt: new Date('2026-05-14T08:30:00.000Z'),
};

describe('AdminUsersReader.findUsers', () => {
  it('issues a COUNT + SELECT in a single Prisma transaction', async () => {
    const prisma = buildPrisma({ count: 5, items: [ROW] });
    const reader = await createSubject(prisma);
    const page = await reader.findUsers({});
    expect(prisma.$transaction).toHaveBeenCalledTimes(1);
    expect(prisma.userDirectoryEntry.count).toHaveBeenCalledTimes(1);
    expect(prisma.userDirectoryEntry.findMany).toHaveBeenCalledTimes(1);
    expect(page.total).toBe(5);
    expect(page.items).toHaveLength(1);
  });

  it('projects rows into the SPA-facing shape (ISO timestamps)', async () => {
    const prisma = buildPrisma({ items: [ROW] });
    const reader = await createSubject(prisma);
    const page = await reader.findUsers({});
    expect(page.items[0]).toEqual({
      oid: 'user-oid',
      tid: 'tenant-1',
      audience: 'workforce',
      username: 'jane.doe@apf.example',
      displayName: 'Jane Doe',
      firstSeenAt: '2026-05-10T10:00:00.000Z',
      lastSeenAt: '2026-05-14T08:30:00.000Z',
    });
  });

  it('orders by last_seen_at DESC, oid ASC for deterministic pagination', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({});
    const findManyArgs = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as {
      orderBy: ReadonlyArray<Record<string, string>>;
    };
    expect(findManyArgs.orderBy).toEqual([{ lastSeenAt: 'desc' }, { oid: 'asc' }]);
  });

  it('passes startsWith filter on username through to Prisma', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({ username: 'jane' });
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as {
      where: { username?: { startsWith?: string } };
    };
    expect(args.where.username).toEqual({ startsWith: 'jane' });
  });

  it('passes case-insensitive contains filter on displayName through to Prisma', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({ displayName: 'doe' });
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as {
      where: { displayName?: { contains?: string; mode?: string } };
    };
    expect(args.where.displayName).toEqual({ contains: 'doe', mode: 'insensitive' });
  });

  it('combines lastSeenAtFrom and lastSeenAtTo into a single gte/lt range filter', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({
      lastSeenAtFrom: '2026-05-01T00:00:00.000Z',
      lastSeenAtTo: '2026-05-14T23:59:59.999Z',
    });
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as {
      where: { lastSeenAt?: { gte?: Date; lt?: Date } };
    };
    expect(args.where.lastSeenAt?.gte).toBeInstanceOf(Date);
    expect(args.where.lastSeenAt?.lt).toBeInstanceOf(Date);
    expect(args.where.lastSeenAt?.gte?.toISOString()).toBe('2026-05-01T00:00:00.000Z');
    expect(args.where.lastSeenAt?.lt?.toISOString()).toBe('2026-05-14T23:59:59.999Z');
  });

  it('applies default limit (50) and offset (0) when not provided', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    const page = await reader.findUsers({});
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as {
      take: number;
      skip: number;
    };
    expect(args.take).toBe(50);
    expect(args.skip).toBe(0);
    expect(page.limit).toBe(50);
    expect(page.offset).toBe(0);
  });

  it('caps limit at MAX_LIMIT (200) even when a caller bypasses the DTO', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({ limit: 1000 });
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as { take: number };
    expect(args.take).toBe(200);
  });

  it('emits no WHERE clause when no filter is provided', async () => {
    const prisma = buildPrisma();
    const reader = await createSubject(prisma);
    await reader.findUsers({});
    const args = prisma.userDirectoryEntry.findMany.mock.calls[0]?.[0] as { where: object };
    expect(args.where).toEqual({});
  });
});
