import { subject } from '@casl/ability';
import { CaslAbilityFactory } from './casl-ability.factory';
import {
  CaslAction,
  SubjectName,
  permissionCodeSubject,
  mapPermissionCode,
  isKnownPermissionCode,
} from './casl.constants';

describe('CaslAbilityFactory', () => {
  const factory = new CaslAbilityFactory();

  // -----------------------------------------------------------------------

  it('grants manage all for super admin regardless of permissions list', () => {
    const ability = factory.createForUser({
      permissions: [],
      isSuperAdmin: true,
    });

    expect(ability.can(CaslAction.Manage, 'all')).toBe(true);
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true);
    expect(ability.can(CaslAction.Delete, SubjectName.Class)).toBe(true);
  });

  it('super admin ability can manage arbitrary subject strings', () => {
    const ability = factory.createForUser({ permissions: [], isSuperAdmin: true });
    expect(ability.can(CaslAction.Manage, 'FictionalEntity')).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  Exact-code permissions (layer 1 — collision-free)
  // -----------------------------------------------------------------------

  it('grants exact-code access for specific permission codes', () => {
    const ability = factory.createForUser({
      permissions: ['bill:view'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:view'))).toBe(true);
  });

  it('does NOT grant exact-code access for a different code in same domain', () => {
    const ability = factory.createForUser({
      permissions: ['bill:view'],
      isSuperAdmin: false,
    });

    // bill:view user should NOT have bill:export-excel exact code
    expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:export-excel'))).toBe(false);
  });

  it('does NOT allow export to satisfy view or vice versa', () => {
    const viewUser = factory.createForUser({
      permissions: ['attendance:view'],
      isSuperAdmin: false,
    });
    const exportUser = factory.createForUser({
      permissions: ['attendance:export'],
      isSuperAdmin: false,
    });

    expect(viewUser.can(CaslAction.Access, permissionCodeSubject('attendance:export'))).toBe(false);
    expect(exportUser.can(CaslAction.Access, permissionCodeSubject('attendance:view'))).toBe(false);
  });

  it('does NOT allow edit to satisfy approve on same resource', () => {
    const editor = factory.createForUser({
      permissions: ['deposit:edit'],
      isSuperAdmin: false,
    });

    expect(editor.can(CaslAction.Access, permissionCodeSubject('deposit:approve'))).toBe(false);
  });

  it('custom/unknown codes get exact-code ability but NO domain ability', () => {
    const ability = factory.createForUser({
      permissions: ['student:nuke'],
      isSuperAdmin: false,
    });

    // Exact code should be granted
    expect(ability.can(CaslAction.Access, permissionCodeSubject('student:nuke'))).toBe(true);

    // But no domain ability should exist
    expect(ability.can(CaslAction.Manage, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(false);
  });

  it('unknown resource custom code gets exact-code but no domain', () => {
    const ability = factory.createForUser({
      permissions: ['custom:action'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Access, permissionCodeSubject('custom:action'))).toBe(true);
    // No domain ability for unknown resource
    const hasAnyDomain = [SubjectName.Student, SubjectName.Bill, SubjectName.Class].some((s) =>
      ability.can(CaslAction.Read, s),
    );
    expect(hasAnyDomain).toBe(false);
  });

  // -----------------------------------------------------------------------
  //  Domain-level permissions (layer 2 — for service scoping)
  // -----------------------------------------------------------------------

  it('maps student:view to domain read Student', () => {
    const ability = factory.createForUser({
      permissions: ['student:view'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true);
    expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false);
  });

  it('maps student:edit to domain update Student', () => {
    const ability = factory.createForUser({
      permissions: ['student:edit'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(true);
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(false);
  });

  it('maps student:create to domain create Student', () => {
    const ability = factory.createForUser({
      permissions: ['student:create'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(true);
  });

  it('maps student:delete to domain delete Student', () => {
    const ability = factory.createForUser({
      permissions: ['student:delete'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(true);
  });

  it('does not broaden occupancy:checkin into generic create ability', () => {
    const ability = factory.createForUser({
      permissions: ['occupancy:checkin'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Create, SubjectName.Occupancy)).toBe(false);
  });

  it('does not broaden bill:export-excel into generic read ability', () => {
    const ability = factory.createForUser({
      permissions: ['bill:export-excel'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Read, SubjectName.Bill)).toBe(false);
    // But exact code is separate
    expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:export-excel'))).toBe(true);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:view'))).toBe(false);
  });

  it('cumulative: multiple permissions all apply at both layers', () => {
    const ability = factory.createForUser({
      permissions: ['student:view', 'room:create', 'bill:delete'],
      isSuperAdmin: false,
    });

    // Domain layer
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true);
    expect(ability.can(CaslAction.Create, SubjectName.Room)).toBe(true);
    expect(ability.can(CaslAction.Delete, SubjectName.Bill)).toBe(true);

    // Exact-code layer
    expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('room:create'))).toBe(true);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:delete'))).toBe(true);
  });

  it('recognizes CASL subject instances instead of treating them as all', () => {
    const ability = factory.createForUser({
      permissions: ['student:view'],
      isSuperAdmin: false,
    });
    const student = subject(SubjectName.Student, { id: 1, classId: 7 });

    expect(ability.can(CaslAction.Read, student)).toBe(true);
    expect(ability.can(CaslAction.Update, student)).toBe(false);
  });

  it('does not let special exact-code permissions grant generic CRUD policies', () => {
    const ability = factory.createForUser({
      permissions: ['student:import', 'deposit:approve', 'bill:confirm'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Update, SubjectName.Deposit)).toBe(false);
    expect(ability.can(CaslAction.Update, SubjectName.Bill)).toBe(false);
  });

  it('does not broaden archive into generic delete ability', () => {
    const ability = factory.createForUser({
      permissions: ['student:archive'],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('student:archive'))).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  Unknown permission codes
  // -----------------------------------------------------------------------

  it('silently ignores unknown codes for domain but grants exact-code access', () => {
    const ability = factory.createForUser({
      permissions: ['unknown:stuff', 'student:view'],
      isSuperAdmin: false,
    });

    // Domain: only known code applies
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true);
    // Exact: both codes get access
    expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('unknown:stuff'))).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  Profile auto-grant
  // -----------------------------------------------------------------------

  it('does not grant broad Profile read without an explicit permission', () => {
    const ability = factory.createForUser({
      permissions: [],
      isSuperAdmin: false,
    });

    expect(ability.can(CaslAction.Read, SubjectName.Profile)).toBe(false);
    // No exact-code access either
    expect(ability.can(CaslAction.Access, permissionCodeSubject('profile:view'))).toBe(false);
  });

  // -----------------------------------------------------------------------
  //  createFromPermissions helper
  // -----------------------------------------------------------------------

  it('createFromPermissions helper works for tests', () => {
    const ability = factory.createFromPermissions(['student:view']);
    expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true);
    expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true);
  });

  it('createFromPermissions helper supports isSuperAdmin flag', () => {
    const ability = factory.createFromPermissions([], true);
    expect(ability.can(CaslAction.Manage, 'all')).toBe(true);
  });
});

// ---------------------------------------------------------------------------
//  Standalone mapping function tests
// ---------------------------------------------------------------------------

describe('mapPermissionCode', () => {
  it('maps known codes', () => {
    expect(mapPermissionCode('student:view')).toEqual({
      action: CaslAction.Read,
      subject: SubjectName.Student,
    });
  });

  it('returns null for unknown resource', () => {
    expect(mapPermissionCode('ghost:action')).toBeNull();
  });

  it('returns null for empty string', () => {
    expect(mapPermissionCode('')).toBeNull();
  });

  it('does not map occupancy:checkin to a generic domain action', () => {
    expect(mapPermissionCode('occupancy:checkin')).toBeNull();
  });

  it('does not map bill:export-excel to generic read', () => {
    expect(mapPermissionCode('bill:export-excel')).toBeNull();
  });

  it('does not map bill:generate to generic update', () => {
    expect(mapPermissionCode('bill:generate')).toBeNull();
  });

  it('does not map deposit:approve to generic update', () => {
    expect(mapPermissionCode('deposit:approve')).toBeNull();
  });

  it('does not map attendance:export to generic read', () => {
    expect(mapPermissionCode('attendance:export')).toBeNull();
  });

  it('does not map archive to generic delete', () => {
    expect(mapPermissionCode('student:archive')).toBeNull();
  });

  it('returns null for unknown action on known resource (student:nuke)', () => {
    expect(mapPermissionCode('student:nuke')).toBeNull();
  });
});

describe('isKnownPermissionCode', () => {
  it('recognizes known codes', () => {
    expect(isKnownPermissionCode('student:view')).toBe(true);
  });

  it('rejects unknown resources', () => {
    expect(isKnownPermissionCode('ghost:action')).toBe(false);
  });
});
