import { ForbiddenException } from '@nestjs/common';
import { AuthorizationService } from './authorization.service';
import { CaslAbilityFactory } from './casl-ability.factory';
import { CaslAction, SubjectName } from './casl.constants';
import { AuthenticatedUser } from './interfaces';

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

  const superAdmin: AuthenticatedUser = {
    id: 1,
    username: 'admin',
    permissions: [],
    isSuperAdmin: true,
    roles: ['超管'],
  };

  const teacher: AuthenticatedUser = {
    id: 2,
    username: 'teacher',
    permissions: ['student:view', 'class:view'],
    isSuperAdmin: false,
    roles: ['老师'],
  };

  const emptyUserReq = (user: AuthenticatedUser) => ({
    user,
  });

  // -----------------------------------------------------------------------
  //  HTTP convenience methods
  // -----------------------------------------------------------------------

  describe('can() — HTTP request convenience', () => {
    it('returns true for super admin on any action/subject', () => {
      expect(service.can(emptyUserReq(superAdmin), CaslAction.Manage, SubjectName.Student)).toBe(
        true,
      );
      expect(service.can(emptyUserReq(superAdmin), CaslAction.Delete, 'all')).toBe(true);
    });

    it('returns true for user with matching permission', () => {
      expect(service.can(emptyUserReq(teacher), CaslAction.Read, SubjectName.Student)).toBe(true);
    });

    it('returns false for user without matching permission', () => {
      expect(service.can(emptyUserReq(teacher), CaslAction.Create, SubjectName.Student)).toBe(
        false,
      );
      expect(service.can(emptyUserReq(teacher), CaslAction.Read, SubjectName.Bill)).toBe(false);
    });
  });

  describe('assert() — HTTP request convenience', () => {
    it('does not throw for super admin', () => {
      expect(() =>
        service.assert(emptyUserReq(superAdmin), CaslAction.Delete, SubjectName.Room),
      ).not.toThrow();
    });

    it('does not throw for user with permission', () => {
      expect(() =>
        service.assert(emptyUserReq(teacher), CaslAction.Read, SubjectName.Student),
      ).not.toThrow();
    });

    it('throws ForbiddenException for user without permission', () => {
      expect(() =>
        service.assert(emptyUserReq(teacher), CaslAction.Create, SubjectName.Student),
      ).toThrow(ForbiddenException);
    });
  });

  // -----------------------------------------------------------------------
  //  Non-HTTP reuse (Agent Tool / background job pattern)
  // -----------------------------------------------------------------------

  describe('abilityForRequest()', () => {
    it('builds ability from request.user', () => {
      const ability = service.abilityForRequest(emptyUserReq(teacher));
      expect(ability.can(CaslAction.Read, SubjectName.Class)).toBe(true);
      expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false);
    });
  });

  describe('canAbility() / assertAbility() — non-HTTP usage', () => {
    const ability = factory.createForUser(teacher);

    it('canAbility returns boolean', () => {
      expect(service.canAbility(ability, CaslAction.Read, SubjectName.Student)).toBe(true);
      expect(service.canAbility(ability, CaslAction.Delete, SubjectName.Student)).toBe(false);
    });

    it('assertAbility throws on denial', () => {
      expect(() =>
        service.assertAbility(ability, CaslAction.Read, SubjectName.Student),
      ).not.toThrow();

      expect(() => service.assertAbility(ability, CaslAction.Delete, SubjectName.Student)).toThrow(
        ForbiddenException,
      );
    });

    it('canAbility and assertAbility work independently of HTTP context', () => {
      // This is the key Agent Tool pattern:
      // 1. Build ability from a user object (no req needed)
      const toolAbility = factory.createForUser({
        permissions: ['attendance:view', 'attendance:create'],
        isSuperAdmin: false,
      });

      // 2. Check / assert using the service
      expect(service.canAbility(toolAbility, CaslAction.Read, SubjectName.Attendance)).toBe(true);
      expect(service.canAbility(toolAbility, CaslAction.Create, SubjectName.Attendance)).toBe(true);
      expect(service.canAbility(toolAbility, CaslAction.Delete, SubjectName.Attendance)).toBe(
        false,
      );

      // 3. assertAbility for write operations
      expect(() =>
        service.assertAbility(toolAbility, CaslAction.Create, SubjectName.Attendance),
      ).not.toThrow();
      expect(() =>
        service.assertAbility(toolAbility, CaslAction.Delete, SubjectName.Attendance),
      ).toThrow(ForbiddenException);
    });
  });

  // -----------------------------------------------------------------------
  //  canPermission() / assertPermission() — exact-code permission checks
  // -----------------------------------------------------------------------

  describe('canPermission() / assertPermission() — exact-code checks', () => {
    const ability = factory.createForUser(teacher);

    it('canPermission returns true for owned exact permission code', () => {
      expect(service.canPermission(ability, 'student:view')).toBe(true);
      expect(service.canPermission(ability, 'class:view')).toBe(true);
    });

    it('canPermission returns false for unowned exact permission code', () => {
      expect(service.canPermission(ability, 'student:delete')).toBe(false);
      expect(service.canPermission(ability, 'bill:view')).toBe(false);
    });

    it('canPermission uses Access + permissionCodeSubject, not domain action', () => {
      // teacher has student:view and class:view. Custom code check is exact.
      expect(service.canPermission(ability, 'student:export')).toBe(false);
    });

    it('assertPermission does not throw for owned code', () => {
      expect(() => service.assertPermission(ability, 'student:view')).not.toThrow();
    });

    it('assertPermission throws ForbiddenException for unowned code', () => {
      expect(() => service.assertPermission(ability, 'student:delete')).toThrow(
        ForbiddenException,
      );
    });

    it('assertPermission error message includes permission code', () => {
      expect(() => service.assertPermission(ability, 'bill:view')).toThrow(
        /bill:view/,
      );
    });

    it('super admin canPermission returns true for any code', () => {
      const saAbility = factory.createForUser(superAdmin);
      expect(service.canPermission(saAbility, 'student:view')).toBe(true);
      expect(service.canPermission(saAbility, 'custom:action')).toBe(true);
      expect(service.canPermission(saAbility, 'bill:export-excel')).toBe(true);
    });

    it('super admin assertPermission never throws', () => {
      const saAbility = factory.createForUser(superAdmin);
      expect(() => service.assertPermission(saAbility, 'ghost:action')).not.toThrow();
    });
  });
});
