import type { Logger } from 'nestjs-pino';
import type { PrismaService } from 'nestjs-prisma';
import { PrismaScopeResolver, toScope } from './prisma-scope-resolver';

interface PrismaStub {
  userScope: {
    findMany: 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?: { findMany?: jest.Mock }): {
  resolver: PrismaScopeResolver;
  prisma: PrismaStub;
  logger: ReturnType<typeof makeLogger>;
} {
  const prisma: PrismaStub = {
    userScope: {
      findMany: opts?.findMany ?? jest.fn().mockResolvedValue([]),
    },
  };
  const logger = makeLogger();
  const resolver = new PrismaScopeResolver(prisma as unknown as PrismaService, logger);
  return { resolver, prisma, logger };
}

describe('PrismaScopeResolver.resolve — query shape', () => {
  it('filters by userId and excludes rows whose expiresAt has passed', async () => {
    const { resolver, prisma } = makeSubject();
    await resolver.resolve({ userId: 'user-uuid-1' });
    expect(prisma.userScope.findMany).toHaveBeenCalledTimes(1);
    const args = prisma.userScope.findMany.mock.calls[0]?.[0] as {
      where: {
        userId: string;
        OR: ReadonlyArray<{ expiresAt: Date | { gt: Date } | null }>;
      };
      select: { kind: boolean; value: boolean };
    };
    expect(args.where.userId).toBe('user-uuid-1');
    expect(args.where.OR).toHaveLength(2);
    expect(args.where.OR[0]).toEqual({ expiresAt: null });
    // The second predicate carries a `gt: <Date>` matching "expiresAt > NOW()".
    expect(args.where.OR[1]).toMatchObject({ expiresAt: { gt: expect.any(Date) } });
    expect(args.select).toEqual({ kind: true, value: true });
  });

  it('returns an empty array when the user has no scope rows', async () => {
    const { resolver } = makeSubject();
    const result = await resolver.resolve({ userId: 'user-uuid-1' });
    expect(result).toEqual([]);
  });
});

describe('PrismaScopeResolver.resolve — row-to-Scope mapping', () => {
  it('maps valueless kinds to `{ kind }`', async () => {
    const findMany = jest.fn().mockResolvedValue([
      { kind: 'self', value: '' },
      { kind: 'siege', value: '' },
      { kind: 'unrestricted', value: '' },
    ]);
    const { resolver } = makeSubject({ findMany });
    const result = await resolver.resolve({ userId: 'u' });
    expect(result).toEqual([{ kind: 'self' }, { kind: 'siege' }, { kind: 'unrestricted' }]);
  });

  it('maps value-bearing kinds to `{ kind, value }`', async () => {
    const findMany = jest.fn().mockResolvedValue([
      { kind: 'etablissement', value: '0330800013' },
      { kind: 'delegation', value: '33' },
      { kind: 'region', value: '75' },
    ]);
    const { resolver } = makeSubject({ findMany });
    const result = await resolver.resolve({ userId: 'u' });
    expect(result).toEqual([
      { kind: 'etablissement', value: '0330800013' },
      { kind: 'delegation', value: '33' },
      { kind: 'region', value: '75' },
    ]);
  });

  it('skips + warns on off-catalogue kinds (defense in depth — drift gate is the canonical guard)', async () => {
    const findMany = jest.fn().mockResolvedValue([
      { kind: 'self', value: '' },
      { kind: 'phantom-kind', value: '' },
      { kind: 'unrestricted', value: '' },
    ]);
    const { resolver, logger } = makeSubject({ findMany });
    const result = await resolver.resolve({ userId: 'u' });
    expect(result).toEqual([{ kind: 'self' }, { kind: 'unrestricted' }]);
    expect(logger.warn).toHaveBeenCalledWith(
      expect.objectContaining({
        event: 'scope_resolver.unknown_kind',
        userId: 'u',
        kind: 'phantom-kind',
      }),
      'PrismaScopeResolver',
    );
  });
});

describe('toScope — pure mapping helper', () => {
  it('returns null for off-catalogue kinds', () => {
    expect(toScope('phantom-kind', '')).toBeNull();
    expect(toScope('', '')).toBeNull();
    expect(toScope('Etablissement', '0330800013')).toBeNull(); // case-sensitive
  });

  it('drops the value for valueless kinds (even if a stale value is in the row)', () => {
    expect(toScope('self', 'stale')).toEqual({ kind: 'self' });
    expect(toScope('siege', 'stale')).toEqual({ kind: 'siege' });
    expect(toScope('unrestricted', 'stale')).toEqual({ kind: 'unrestricted' });
  });

  it('preserves the value verbatim for value-bearing kinds', () => {
    expect(toScope('etablissement', '0330800013')).toEqual({
      kind: 'etablissement',
      value: '0330800013',
    });
    expect(toScope('delegation', '2A')).toEqual({ kind: 'delegation', value: '2A' });
    expect(toScope('region', '75')).toEqual({ kind: 'region', value: '75' });
  });
});
