import { BadRequestException, NotFoundException } from '@nestjs/common';
import type { Logger } from 'nestjs-pino';
import type { PrismaService } from 'nestjs-prisma';
import { UserScopesService } from './user-scopes.service';

interface PrismaStub {
  user: { findUnique: jest.Mock };
  userScope: {
    findMany: jest.Mock;
    findUnique: jest.Mock;
    create: jest.Mock;
    delete: jest.Mock;
  };
  structure: { findUnique: jest.Mock };
  delegation: { findUnique: jest.Mock };
  region: { findUnique: 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 makePrisma(overrides?: Partial<PrismaStub>): PrismaStub {
  return {
    user: { findUnique: jest.fn().mockResolvedValue(null), ...overrides?.user },
    userScope: {
      findMany: jest.fn().mockResolvedValue([]),
      findUnique: jest.fn().mockResolvedValue(null),
      create: jest.fn(),
      delete: jest.fn().mockResolvedValue(undefined),
      ...overrides?.userScope,
    },
    structure: { findUnique: jest.fn().mockResolvedValue(null), ...overrides?.structure },
    delegation: { findUnique: jest.fn().mockResolvedValue(null), ...overrides?.delegation },
    region: { findUnique: jest.fn().mockResolvedValue(null), ...overrides?.region },
  };
}

function makeSubject(overrides?: Partial<PrismaStub>): {
  service: UserScopesService;
  prisma: PrismaStub;
  logger: ReturnType<typeof makeLogger>;
} {
  const prisma = makePrisma(overrides);
  const logger = makeLogger();
  const service = new UserScopesService(prisma as unknown as PrismaService, logger);
  return { service, prisma, logger };
}

const FOUND_USER = {
  id: 'user-uuid-1',
  person: { firstName: 'Jane', lastName: 'Doe', email: 'jane@apf.example' },
};

describe('UserScopesService.resolveUserByOid', () => {
  it('returns the User + composed displayName when found', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
    });
    const result = await service.resolveUserByOid('entra-oid');
    expect(result).toEqual({
      oid: 'entra-oid',
      userId: 'user-uuid-1',
      displayName: 'Jane Doe',
      email: 'jane@apf.example',
    });
  });

  it('throws NotFoundException when the User does not exist', async () => {
    const { service } = makeSubject();
    await expect(service.resolveUserByOid('ghost-oid')).rejects.toBeInstanceOf(NotFoundException);
  });

  it('falls back to "(unnamed)" when both firstName and lastName are empty', async () => {
    const { service } = makeSubject({
      user: {
        findUnique: jest.fn().mockResolvedValue({
          id: 'u',
          person: { firstName: '', lastName: '', email: null },
        }),
      },
    });
    const result = await service.resolveUserByOid('o');
    expect(result.displayName).toBe('(unnamed)');
    expect(result.email).toBeNull();
  });
});

describe('UserScopesService.list', () => {
  it('returns scopes ordered by kind then value with ISO timestamps', async () => {
    const { service, prisma } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      userScope: {
        findMany: jest.fn().mockResolvedValue([
          {
            id: 's1',
            kind: 'etablissement',
            value: '0330800013',
            source: 'seed',
            createdAt: new Date('2026-05-26T10:00:00.000Z'),
            expiresAt: null,
          },
        ]),
        findUnique: jest.fn(),
        create: jest.fn(),
        delete: jest.fn(),
      },
    });
    const result = await service.list('entra-oid');
    expect(prisma.userScope.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { userId: 'user-uuid-1' },
        orderBy: [{ kind: 'asc' }, { value: 'asc' }],
      }),
    );
    expect(result.scopes).toEqual([
      {
        id: 's1',
        kind: 'etablissement',
        value: '0330800013',
        source: 'seed',
        createdAt: '2026-05-26T10:00:00.000Z',
        expiresAt: null,
      },
    ]);
  });
});

describe('UserScopesService.grant — value-bearing kinds', () => {
  it('creates the row when the etablissement value matches an existing Structure', async () => {
    const { service, prisma } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      structure: { findUnique: jest.fn().mockResolvedValue({ code: '0330800013' }) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn(),
        delete: jest.fn(),
        create: jest.fn().mockResolvedValue({
          id: 'new-scope',
          kind: 'etablissement',
          value: '0330800013',
          source: 'admin-ui',
          createdAt: new Date('2026-05-26T10:00:00.000Z'),
          expiresAt: null,
        }),
      },
    });
    const out = await service.grant('entra-oid', {
      kind: 'etablissement',
      value: '0330800013',
    });
    expect(prisma.structure.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({ where: { code: '0330800013' } }),
    );
    expect(prisma.userScope.create).toHaveBeenCalledWith({
      data: {
        userId: 'user-uuid-1',
        kind: 'etablissement',
        value: '0330800013',
        source: 'admin-ui',
        expiresAt: null,
      },
      select: expect.any(Object),
    });
    expect(out.scope.kind).toBe('etablissement');
    expect(out.scope.value).toBe('0330800013');
    expect(out.scope.source).toBe('admin-ui');
  });

  it('rejects when the value does not match any Structure row', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      structure: { findUnique: jest.fn().mockResolvedValue(null) },
    });
    await expect(
      service.grant('entra-oid', { kind: 'etablissement', value: 'bogus' }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('rejects when the value is empty for a value-bearing kind', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
    });
    await expect(
      service.grant('entra-oid', { kind: 'delegation', value: '' }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('validates delegation values against the Delegation table', async () => {
    const { service, prisma } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      delegation: { findUnique: jest.fn().mockResolvedValue({ code: '33' }) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn(),
        delete: jest.fn(),
        create: jest.fn().mockResolvedValue({
          id: 's',
          kind: 'delegation',
          value: '33',
          source: 'admin-ui',
          createdAt: new Date(),
          expiresAt: null,
        }),
      },
    });
    await service.grant('entra-oid', { kind: 'delegation', value: '33' });
    expect(prisma.delegation.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({ where: { code: '33' } }),
    );
  });
});

describe('UserScopesService.grant — valueless kinds', () => {
  it('writes value="" for unrestricted and rejects non-empty values', async () => {
    const { service, prisma } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn(),
        delete: jest.fn(),
        create: jest.fn().mockResolvedValue({
          id: 's',
          kind: 'unrestricted',
          value: '',
          source: 'admin-ui',
          createdAt: new Date(),
          expiresAt: null,
        }),
      },
    });
    await service.grant('entra-oid', { kind: 'unrestricted' });
    expect(prisma.userScope.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ value: '' }),
      }),
    );
    await expect(
      service.grant('entra-oid', { kind: 'unrestricted', value: 'something' }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });
});

describe('UserScopesService.grant — duplicate detection', () => {
  it('translates Prisma P2002 into a 400 with a friendly message', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      structure: { findUnique: jest.fn().mockResolvedValue({ code: '0330800013' }) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn(),
        delete: jest.fn(),
        create: jest.fn().mockRejectedValue(Object.assign(new Error('unique'), { code: 'P2002' })),
      },
    });
    await expect(
      service.grant('entra-oid', { kind: 'etablissement', value: '0330800013' }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });
});

describe('UserScopesService.revoke', () => {
  it('deletes the row and returns the resolved tuple for audit', async () => {
    const { service, prisma } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn().mockResolvedValue({
          id: 's1',
          userId: 'user-uuid-1',
          kind: 'delegation',
          value: '33',
        }),
        create: jest.fn(),
        delete: jest.fn().mockResolvedValue(undefined),
      },
    });
    const out = await service.revoke('entra-oid', 's1');
    expect(prisma.userScope.delete).toHaveBeenCalledWith({ where: { id: 's1' } });
    expect(out.revoked).toEqual({ kind: 'delegation', value: '33' });
  });

  it('throws NotFoundException when the scope belongs to a different user', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
      userScope: {
        findMany: jest.fn(),
        findUnique: jest.fn().mockResolvedValue({
          id: 's1',
          userId: 'other-user',
          kind: 'delegation',
          value: '33',
        }),
        create: jest.fn(),
        delete: jest.fn(),
      },
    });
    await expect(service.revoke('entra-oid', 's1')).rejects.toBeInstanceOf(NotFoundException);
  });

  it('throws NotFoundException when no scope exists for that id', async () => {
    const { service } = makeSubject({
      user: { findUnique: jest.fn().mockResolvedValue(FOUND_USER) },
    });
    await expect(service.revoke('entra-oid', 'ghost')).rejects.toBeInstanceOf(NotFoundException);
  });
});
