import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ClsService } from 'nestjs-cls';
import { PermissionContextService } from './permission-context.service';
import {
  RolePermissionEntity,
  AccessLevel,
  ModuleName,
} from '../../entities/role-permission.entity';
import { CLS_USER_ROLE_ID, CLS_TENANT_ID } from '../cls/cls.constants';

describe('PermissionContextService', () => {
  let service: PermissionContextService;
  let repoFindOne: jest.Mock;
  let clsGet: jest.Mock;

  const ROLE = 'role-1';
  const TENANT = 'tenant-1';

  beforeEach(async () => {
    repoFindOne = jest.fn();
    clsGet = jest.fn((key: string) => {
      if (key === CLS_USER_ROLE_ID) return ROLE;
      if (key === CLS_TENANT_ID) return TENANT;
      return null;
    });
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PermissionContextService,
        { provide: getRepositoryToken(RolePermissionEntity), useValue: { findOne: repoFindOne } },
        { provide: ClsService, useValue: { get: clsGet } },
      ],
    }).compile();
    service = module.get(PermissionContextService);
  });

  afterEach(() => jest.clearAllMocks());

  it('returns false when role_id is missing from CLS (no auth context)', async () => {
    clsGet.mockImplementation(() => null);
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.VIEW)).toBe(false);
    expect(repoFindOne).not.toHaveBeenCalled();
  });

  it('returns false when no role_permission row exists', async () => {
    repoFindOne.mockResolvedValue(null);
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.VIEW)).toBe(false);
  });

  // Regression — TypeORM 1.0 mis-generates SQL when a partial `select`
  // is combined with a relation filter (`role: { id }`); the inner
  // DISTINCT subquery omits the entity's id column and Postgres throws
  // `column "RolePermissionEntity_id" does not exist` (code 42703).
  // Make sure the repo lookup does NOT include a partial `select`.
  it('does NOT pass a partial `select` clause to findOne (TypeORM 1.0 DISTINCT bug)', async () => {
    repoFindOne.mockResolvedValue({ access_level: AccessLevel.VIEW });
    await service.hasPermission(ModuleName.HOTEL, AccessLevel.VIEW);
    const args = repoFindOne.mock.calls[0][0];
    expect(args).not.toHaveProperty('select');
  });

  it('grants when the user level meets the required level', async () => {
    repoFindOne.mockResolvedValue({ access_level: AccessLevel.EDIT });
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.CREATE)).toBe(true);
  });

  it('denies when the user level is below the required level', async () => {
    repoFindOne.mockResolvedValue({ access_level: AccessLevel.VIEW });
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.EDIT)).toBe(false);
  });

  it('treats DELETE as the highest level (covers everything)', async () => {
    repoFindOne.mockResolvedValue({ access_level: AccessLevel.DELETE });
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.EDIT)).toBe(true);
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.VIEW)).toBe(true);
  });

  it('treats NONE as no access', async () => {
    repoFindOne.mockResolvedValue({ access_level: AccessLevel.NONE });
    expect(await service.hasPermission(ModuleName.HOTEL, AccessLevel.VIEW)).toBe(false);
  });
});
