import { filter, first } from 'rxjs/operators';
import { AUTH_OPTIONS, AuthenticationService, IMacmaAuthModuleConfig, MacmaAuthorizationService } from '../../';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { BehaviorSubject } from 'rxjs';
import { TestBed } from '@angular/core/testing';
import { Provider } from '@angular/core';
import { expectValues } from '../../../test-setup';

const userAclRequestUrl = 'http://test/access-management/v1/user/acl';

const TENANT_ID = 'tenant-id';
const ROLE = 'urn:macma-application-role:' + TENANT_ID + ':myapplicationId:user-reader';
const RESOURCE_ID = 'myresource';
const APPLICATION_ID = 'myapplicationId';
const RESOURCE_TYPE = 'myresourceType';

describe('MacmaAuthorizationService - with deprecated accessControlEndpoint parameter', () => {
  let service: MacmaAuthorizationService;
  let httpTestingController: HttpTestingController;
  let request;
  let rolesSubject: BehaviorSubject<string[]>;
  const isAuthenticateSubject = new BehaviorSubject(true);
  const refreshInterval = 50;
  const responseAcl = {
    items: [
      {
        resourceId: RESOURCE_ID,
        resourceType: RESOURCE_TYPE,
        resourceOwningTenantId: TENANT_ID,
        applicationId: APPLICATION_ID,
        grants: [
          {
            roleName: ROLE,
            privileges: ['read', 'add'],
          },
        ],
      },
    ],
  };

  beforeEach(() => {
    rolesSubject = new BehaviorSubject([ROLE, 'test2']);
    const authenticationServiceStub = {
      getRoles: () => rolesSubject.asObservable(),
      isAuthenticated: () => isAuthenticateSubject,
      getTenantId: () => TENANT_ID,
    };

    TestBed.configureTestingModule({
      providers: <Provider[]>[
        {
          provide: AUTH_OPTIONS,
          useValue: <IMacmaAuthModuleConfig>{
            accessControlEndpoint: 'http://test',
            resourceSetName: 'test',
            aclReloadIntervalMs: refreshInterval, // for production this would be much too often, but its good for fast tests as we failed to use fake timers
          },
        },
        {
          provide: AuthenticationService,
          useFactory: () => authenticationServiceStub,
        },
        {
          provide: MacmaAuthorizationService,
          useClass: MacmaAuthorizationService,
        },
      ],
      imports: [HttpClientTestingModule],
    });
    service = TestBed.inject(MacmaAuthorizationService);
    httpTestingController = TestBed.inject(HttpTestingController);

    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(responseAcl);
  });

  it('should init', () => {
    expect(service).toBeTruthy();
  });

  const expectRegularRefreshInterval = async function (acl = responseAcl) {
    // after half the refresh interval
    await new Promise((r) => setTimeout(r, refreshInterval / 2 + 2));
    // expect no refresh yet
    httpTestingController.expectNone(userAclRequestUrl);
    httpTestingController.verify();

    // after the first refresh interval has passed (2*25 + 2 = 52)
    await new Promise((r) => setTimeout(r, refreshInterval / 2));
    // expect another request
    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(acl);
    httpTestingController.verify();
  };

  const expectRefreshIntervalResetAfterTriggeringLoad = async function (triggerReload: () => void) {
    // trigger refresh after half a refresh interval
    await new Promise((r) => setTimeout(r, refreshInterval / 2));
    triggerReload();
    // expect another request triggered by auth toggle
    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(responseAcl);
    httpTestingController.verify();

    await expectRegularRefreshInterval();
  };

  it('should refresh regularly', async () => {
    httpTestingController.verify();
    await expectRegularRefreshInterval();
    await expectRegularRefreshInterval();
  });

  it('should reset refresh interval upon refresh', async () => {
    httpTestingController.verify();
    await expectRegularRefreshInterval();
    await expectRefreshIntervalResetAfterTriggeringLoad(() => service.refreshSubject.next(void 0));
  });

  it('should reset refresh interval upon toggling auth off and on again', async () => {
    await expectRegularRefreshInterval();
    await expectRefreshIntervalResetAfterTriggeringLoad(() => {
      isAuthenticateSubject.next(false);
      isAuthenticateSubject.next(true);
    });
  });

  it('should load acl and verify permissions', async () => {
    expect(request.request.method).toBe('GET');

    httpTestingController.verify();

    expect(service.canRead(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canAdd(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canModify(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canDelete(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canExecute(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
  });

  it('should load acl and set aclLoaded to true', (done) => {
    expect(request.request.method).toBe('GET');

    httpTestingController.verify();

    service.aclLoaded$
      .pipe(
        filter((loaded) => loaded === true),
        first()
      )
      .subscribe({ complete: () => done() });
  });

  it('should load acl and verify permissions', () => {
    expect(request.request.method).toBe('GET');

    httpTestingController.verify();

    expect(service.canRead(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canAdd(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canModify(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canDelete(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canExecute(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
  });

  it('should emit on role change', (done) => {
    service
      .canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)
      .subscribe(expectValues([true, false, true], done));
    rolesSubject.next(['testrole']);
    rolesSubject.next([ROLE, 'testrole']);
  });

  it('should emit on acl refresh', (done) => {
    service
      .canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)
      .subscribe(expectValues([true, false], done));
    expectRegularRefreshInterval({
      items: [
        {
          resourceId: RESOURCE_ID,
          resourceType: RESOURCE_TYPE,
          resourceOwningTenantId: TENANT_ID,
          applicationId: APPLICATION_ID,
          grants: [
            {
              // role ROLE is no longer allowed to read the resource
              roleName: 'anotherrole',
              privileges: ['read', 'add'],
            },
          ],
        },
      ],
    });
  });

  it('should allow read', (done) => {
    service.canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeTruthy();
      done();
    });
  });

  it('should allow add', (done) => {
    service.canAddObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeTruthy();
      done();
    });
  });

  it('should not allow execute', (done) => {
    service.canExecuteObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });

  it('should not allow modify', (done) => {
    service.canModifyObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });

  it('should not allow delete', (done) => {
    service.canDeleteObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });
});

describe('MacmaAuthorizationService', () => {
  let service: MacmaAuthorizationService;
  let httpTestingController: HttpTestingController;
  let request;
  let rolesSubject: BehaviorSubject<string[]>;
  const isAuthenticateSubject = new BehaviorSubject(true);
  const refreshInterval = 50;
  const responseAcl = {
    items: [
      {
        resourceId: RESOURCE_ID,
        resourceType: RESOURCE_TYPE,
        resourceOwningTenantId: TENANT_ID,
        applicationId: APPLICATION_ID,
        grants: [
          {
            roleName: ROLE,
            privileges: ['read', 'add'],
          },
        ],
      },
    ],
  };

  beforeEach(() => {
    rolesSubject = new BehaviorSubject([ROLE, 'test2']);
    const authenticationServiceStub = {
      getRoles: () => rolesSubject.asObservable(),
      isAuthenticated: () => isAuthenticateSubject,
      getTenantId: () => TENANT_ID,
    };

    TestBed.configureTestingModule({
      providers: <Provider[]>[
        {
          provide: AUTH_OPTIONS,
          useValue: <IMacmaAuthModuleConfig>{
            authServerBaseUrl: 'http://test',
            resourceSetName: 'test',
            aclReloadIntervalMs: refreshInterval, // for production this would be much too often, but its good for fast tests as we failed to use fake timers
          },
        },
        {
          provide: AuthenticationService,
          useFactory: () => authenticationServiceStub,
        },
        {
          provide: MacmaAuthorizationService,
          useClass: MacmaAuthorizationService,
        },
      ],
      imports: [HttpClientTestingModule],
    });
    service = TestBed.inject(MacmaAuthorizationService);
    httpTestingController = TestBed.inject(HttpTestingController);

    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(responseAcl);
  });

  it('should init', () => {
    expect(service).toBeTruthy();
  });

  const expectRegularRefreshInterval = async function (acl = responseAcl) {
    // after half the refresh interval
    await new Promise((r) => setTimeout(r, refreshInterval / 2 + 2));
    // expect no refresh yet
    httpTestingController.expectNone(userAclRequestUrl);
    httpTestingController.verify();

    // after the first refresh interval has passed (12+10 = 22)
    await new Promise((r) => setTimeout(r, refreshInterval / 2));
    // expect another request
    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(acl);
    httpTestingController.verify();
  };

  const expectRefreshIntervalResetAfterTriggeringLoad = async function (triggerReload: () => void) {
    // trigger refresh after half a refresh interval
    await new Promise((r) => setTimeout(r, refreshInterval / 2));
    triggerReload();
    // expect another request triggered by auth toggle
    request = httpTestingController.expectOne(userAclRequestUrl);
    request.flush(responseAcl);
    httpTestingController.verify();

    await expectRegularRefreshInterval();
  };

  it('should refresh regularly', async () => {
    httpTestingController.verify();
    await expectRegularRefreshInterval();
    await expectRegularRefreshInterval();
  });

  it('should reset refresh interval upon refresh', async () => {
    httpTestingController.verify();
    await expectRegularRefreshInterval();
    await expectRefreshIntervalResetAfterTriggeringLoad(() => service.refreshSubject.next(void 0));
  });

  it('should reset refresh interval upon toggling auth off and on again', async () => {
    await expectRegularRefreshInterval();
    await expectRefreshIntervalResetAfterTriggeringLoad(() => {
      isAuthenticateSubject.next(false);
      isAuthenticateSubject.next(true);
    });
  });

  it('should load acl and verify permissions', async () => {
    expect(request.request.method).toBe('GET');

    httpTestingController.verify();

    expect(service.canRead(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canAdd(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canModify(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canDelete(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canExecute(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
  });

  it('should load acl and verify permissions', () => {
    expect(request.request.method).toBe('GET');

    httpTestingController.verify();

    expect(service.canRead(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canAdd(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeTruthy();
    expect(service.canModify(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canDelete(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
    expect(service.canExecute(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)).toBeFalsy();
  });

  it('should emit on role change', (done) => {
    service
      .canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)
      .subscribe(expectValues([true, false, true], done));
    rolesSubject.next(['testrole']);
    rolesSubject.next([ROLE, 'testrole']);
  });

  it('should emit on acl refresh', (done) => {
    service
      .canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE)
      .subscribe(expectValues([true, false], done));
    expectRegularRefreshInterval({
      items: [
        {
          resourceId: RESOURCE_ID,
          resourceType: RESOURCE_TYPE,
          resourceOwningTenantId: TENANT_ID,
          applicationId: APPLICATION_ID,
          grants: [
            {
              // role ROLE is no longer allowed to read the resource
              roleName: 'anotherrole',
              privileges: ['read', 'add'],
            },
          ],
        },
      ],
    });
  });

  it('should allow read', (done) => {
    service.canReadObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeTruthy();
      done();
    });
  });

  it('should allow add', (done) => {
    service.canAddObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeTruthy();
      done();
    });
  });

  it('should not allow execute', (done) => {
    service.canExecuteObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });

  it('should not allow modify', (done) => {
    service.canModifyObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });

  it('should not allow delete', (done) => {
    service.canDeleteObservable(RESOURCE_ID, TENANT_ID, APPLICATION_ID, RESOURCE_TYPE).subscribe((allowed) => {
      expect(allowed).toBeFalsy();
      done();
    });
  });
});
