import { PoliciesGuard } from './policies.guard';
import { CaslAbilityFactory } from '../casl-ability.factory';
import { AppAbility, IPolicyHandler } from '../interfaces';

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

  /** Build a mock NestJS ExecutionContext for PoliciesGuard */
  function createContext(
    user: unknown,
    opts: {
      policyHandlers?: Array<((ability: AppAbility) => boolean) | IPolicyHandler> | null;
      controllerPolicyHandlers?: Array<((ability: AppAbility) => boolean) | IPolicyHandler>;
      isPublic?: boolean;
    } = {},
  ) {
    const meta = new Map<string, unknown>();
    if (opts.policyHandlers !== undefined) meta.set('check_policies', opts.policyHandlers);
    if (opts.isPublic !== undefined) meta.set('isPublic', opts.isPublic);

    const reflector = {
      getAllAndOverride: jest.fn((key: string) => meta.get(key) ?? undefined),
      getAllAndMerge: jest.fn((key: string) => {
        if (key !== 'check_policies') return [];
        return [...(opts.policyHandlers ?? []), ...(opts.controllerPolicyHandlers ?? [])];
      }),
    };
    const guard = new PoliciesGuard(reflector as never, factory);
    return guard.canActivate({
      getHandler: () => function handler() {},
      getClass: () => class Controller {},
      switchToHttp: () => ({
        getRequest: () => ({ user }),
      }),
    } as never);
  }

  // -----------------------------------------------------------------------
  //  No @CheckPolicies → pass-through
  // -----------------------------------------------------------------------

  it('passes through when no @CheckPolicies is declared', () => {
    expect(createContext(undefined)).toBe(true);
    expect(createContext(null)).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  @Public interaction
  // -----------------------------------------------------------------------

  it('skips when @Public is declared, even with @CheckPolicies', () => {
    expect(
      createContext(undefined, {
        policyHandlers: [(ability) => ability.can('read', 'Student')],
        isPublic: true,
      }),
    ).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  User absent
  // -----------------------------------------------------------------------

  it('denies when @CheckPolicies is declared but no user present', () => {
    expect(
      createContext(undefined, {
        policyHandlers: [(ability) => ability.can('read', 'Student')],
      }),
    ).toBe(false);
  });

  // -----------------------------------------------------------------------
  //  Callback handlers
  // -----------------------------------------------------------------------

  it('grants when all policies pass for super admin', () => {
    expect(
      createContext(
        { permissions: [], isSuperAdmin: true },
        {
          policyHandlers: [
            (ability) => ability.can('read', 'Student'),
            (ability) => ability.can('delete', 'Class'),
          ],
        },
      ),
    ).toBe(true);
  });

  it('grants when all policies pass for user with correct permissions', () => {
    expect(
      createContext(
        { permissions: ['student:view', 'class:view'], isSuperAdmin: false },
        {
          policyHandlers: [
            (ability) => ability.can('read', 'Student'),
            (ability) => ability.can('read', 'Class'),
          ],
        },
      ),
    ).toBe(true);
  });

  it('denies when any policy fails (AND semantics)', () => {
    expect(
      createContext(
        { permissions: ['student:view'], isSuperAdmin: false },
        {
          policyHandlers: [
            (ability) => ability.can('read', 'Student'), // passes
            (ability) => ability.can('delete', 'Student'), // fails
          ],
        },
      ),
    ).toBe(false);
  });

  it('empty handlers array passes (no policies to check)', () => {
    expect(
      createContext({ permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [] }),
    ).toBe(true);
  });

  it('merges controller and handler policies with AND semantics', () => {
    expect(
      createContext(
        { permissions: ['student:view'], isSuperAdmin: false },
        {
          policyHandlers: [(ability) => ability.can('read', 'Student')],
          controllerPolicyHandlers: [(ability) => ability.can('read', 'Class')],
        },
      ),
    ).toBe(false);
  });

  // -----------------------------------------------------------------------
  //  Class-based handlers
  // -----------------------------------------------------------------------

  it('supports class-based policy handlers', () => {
    class ReadStudentPolicy implements IPolicyHandler {
      handle(ability: AppAbility): boolean {
        return ability.can('read', 'Student');
      }
    }

    expect(
      createContext(
        { permissions: ['student:view'], isSuperAdmin: false },
        { policyHandlers: [new ReadStudentPolicy()] },
      ),
    ).toBe(true);
  });

  it('denies when class-based handler fails', () => {
    class DeleteStudentPolicy implements IPolicyHandler {
      handle(ability: AppAbility): boolean {
        return ability.can('delete', 'Student');
      }
    }

    expect(
      createContext(
        { permissions: ['student:view'], isSuperAdmin: false },
        { policyHandlers: [new DeleteStudentPolicy()] },
      ),
    ).toBe(false);
  });

  it('mixes callback and class-based handlers', () => {
    class ReadStudentPolicy implements IPolicyHandler {
      handle(ability: AppAbility): boolean {
        return ability.can('read', 'Student');
      }
    }

    expect(
      createContext(
        { permissions: ['student:view', 'class:view'], isSuperAdmin: false },
        {
          policyHandlers: [new ReadStudentPolicy(), (ability) => ability.can('read', 'Class')],
        },
      ),
    ).toBe(true);
  });

  // -----------------------------------------------------------------------
  //  Instance-level policy
  // -----------------------------------------------------------------------

  it('custom instance-level policy: checks specific resource conditions', () => {
    const ability = factory.createForUser({
      permissions: ['student:edit'],
      isSuperAdmin: false,
    });

    const ownsResource = (ab: typeof ability) => ab.can('update', 'Student');

    expect(ownsResource(ability)).toBe(true);
  });
});
