import type { Logger } from 'nestjs-pino';
import type { PrismaService } from 'nestjs-prisma';
import { UserDirectoryService, type UserDirectoryEntry } from './user-directory.service';

interface PrismaStub {
  user: {
    upsert: jest.Mock;
  };
}

function makeLogger() {
  return { log: jest.fn(), warn: jest.fn(), error: jest.fn() } as unknown as Logger & {
    log: jest.Mock;
    warn: jest.Mock;
    error: jest.Mock;
  };
}

function makeSubject(opts?: { upsert?: jest.Mock }): {
  service: UserDirectoryService;
  prisma: PrismaStub;
  logger: ReturnType<typeof makeLogger>;
} {
  const prisma: PrismaStub = {
    user: {
      upsert: opts?.upsert ?? jest.fn().mockResolvedValue(undefined),
    },
  };
  const logger = makeLogger();
  const service = new UserDirectoryService(prisma as unknown as PrismaService, logger);
  return { service, prisma, logger };
}

const ENTRY: UserDirectoryEntry = {
  oid: 'user-oid',
  tid: 'tenant-1',
  username: 'jane.doe@apf.example',
  displayName: 'Jane Doe',
  audience: 'workforce',
};

describe('UserDirectoryService.recordSignIn', () => {
  it('issues an upsert keyed by oid with the right create / update payloads', async () => {
    const { service, prisma } = makeSubject();
    await service.recordSignIn(ENTRY);
    expect(prisma.user.upsert).toHaveBeenCalledTimes(1);
    const args = prisma.user.upsert.mock.calls[0]?.[0] as {
      where: { oid: string };
      create: Record<string, unknown>;
      update: Record<string, unknown>;
    };
    expect(args.where).toEqual({ oid: 'user-oid' });
    expect(args.create).toEqual({
      oid: 'user-oid',
      tid: 'tenant-1',
      audience: 'workforce',
      username: 'jane.doe@apf.example',
      displayName: 'Jane Doe',
    });
    // Update path: refreshes every mutable field + bumps lastSeenAt;
    // crucially does NOT include `firstSeenAt` so it stays
    // immutable after first sign-in.
    expect(args.update).toEqual(
      expect.objectContaining({
        tid: 'tenant-1',
        audience: 'workforce',
        username: 'jane.doe@apf.example',
        displayName: 'Jane Doe',
      }),
    );
    expect(args.update['firstSeenAt']).toBeUndefined();
    expect(args.update['lastSeenAt']).toBeInstanceOf(Date);
  });

  it('updates `displayName` + `username` if either changed since the previous sign-in', async () => {
    const { service, prisma } = makeSubject();
    await service.recordSignIn({
      ...ENTRY,
      displayName: 'Jane Updated',
      username: 'jane2@apf.example',
    });
    const args = prisma.user.upsert.mock.calls[0]?.[0] as {
      update: Record<string, unknown>;
    };
    expect(args.update['displayName']).toBe('Jane Updated');
    expect(args.update['username']).toBe('jane2@apf.example');
  });

  it('swallows Prisma errors and logs them — never propagates (sign-in must succeed)', async () => {
    const upsert = jest.fn().mockRejectedValue(new Error('connection refused'));
    const { service, logger } = makeSubject({ upsert });
    await expect(service.recordSignIn(ENTRY)).resolves.toBeUndefined();
    expect(logger.warn).toHaveBeenCalledWith(
      expect.objectContaining({
        event: 'user_directory.record_sign_in_failed',
        oid: 'user-oid',
        reason: 'connection refused',
      }),
      'UserDirectoryService',
    );
  });

  it('logs the underlying message verbatim when the rejection is not an Error', async () => {
    const upsert = jest.fn().mockRejectedValue('some string');
    const { service, logger } = makeSubject({ upsert });
    await service.recordSignIn(ENTRY);
    expect(logger.warn).toHaveBeenCalledWith(
      expect.objectContaining({ reason: 'some string' }),
      'UserDirectoryService',
    );
  });
});
