import { Mock, mock, useFromMock } from '@alfa-client/test-utils';
import { HttpErrorResponse } from '@angular/common/http';
import { fakeAsync, tick } from '@angular/core/testing';
import { faker } from '@faker-js/faker';
import { Resource, ResourceUri, getUrl } from '@ngxp/rest';
import { cold } from 'jest-marbles';
import { Observable, lastValueFrom, of, throwError } from 'rxjs';
import { createProblemDetail } from '../../../test//error';
import { singleCold, singleHot } from '../../../test/marbles';
import { createDummyResource } from '../../../test/resource';
import { HttpError, ProblemDetail } from '../tech.model';
import { LinkRelationName, ResourceServiceConfig } from './resource.model';
import { ResourceRepository } from './resource.repository';
import { ResourceService } from './resource.service';
import {
  StateResource,
  createEmptyStateResource,
  createErrorStateResource,
  createStateResource,
} from './resource.util';

import * as ResourceUtil from './resource.util';

describe('ResourceService', () => {
  let service: DummyResourceService<Resource, Resource>;
  let config: ResourceServiceConfig<Resource>;

  let repository: Mock<ResourceRepository>;

  const configResource: Resource = createDummyResource();
  const configStateResource: StateResource<Resource> = createStateResource(configResource);
  const configStateResource$: Observable<StateResource<Resource>> = of(configStateResource);

  const dummyResource: Resource = createDummyResource();
  const dummyStateResource: StateResource<Resource> = createStateResource(dummyResource);

  const editLinkRel: string = 'dummyEditLinkRel';
  const getLinkRel: LinkRelationName = 'dummyGetLinkRel';
  const deleteLinkRel: LinkRelationName = 'dummyDeleteLinkRel';

  beforeEach(() => {
    config = {
      resource: configStateResource$,
      getLinkRel,
      edit: { linkRel: editLinkRel },
      delete: { linkRel: deleteLinkRel },
    };
    repository = mock(ResourceRepository);

    service = new DummyResourceService(config, useFromMock(repository));
  });

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

  describe('get', () => {
    const stateResource: StateResource<Resource> = createStateResource(configResource);
    let isInvalidResourceCombinationSpy: jest.SpyInstance;

    beforeEach(() => {
      service.stateResource.next(stateResource);

      service.handleResourceChanges = jest.fn();
      isInvalidResourceCombinationSpy = jest
        .spyOn(ResourceUtil, 'isInvalidResourceCombination')
        .mockReturnValue(true);
    });

    it('should handle config resource changed', fakeAsync(() => {
      service.get().subscribe();
      tick();

      expect(service.handleResourceChanges).toHaveBeenCalledWith(stateResource, configResource);
    }));

    it('should call isInvalidResourceCombinationSpy', fakeAsync(() => {
      service.get().subscribe();
      tick();

      expect(isInvalidResourceCombinationSpy).toHaveBeenCalled();
    }));

    it('should return initial value', () => {
      service.stateResource.asObservable = jest
        .fn()
        .mockReturnValue(singleHot(stateResource, '-a'));

      const apiRootStateResource$: Observable<StateResource<Resource>> = service.get();

      expect(apiRootStateResource$).toBeObservable(
        cold('a', { a: createEmptyStateResource(true) }),
      );
    });
  });

  describe('handle resource changes', () => {
    const stateResource: StateResource<Resource> = createStateResource(createDummyResource());
    const changedConfigResource: Resource = createDummyResource();

    describe('on different config resource', () => {
      beforeEach(() => {
        service.handleConfigResourceChanges = jest.fn();
      });

      it('should update state resource by config resource', () => {
        service.configResource = createDummyResource();

        service.handleResourceChanges(stateResource, changedConfigResource);

        expect(service.handleConfigResourceChanges).toHaveBeenCalled();
      });
    });

    describe('on same config resource', () => {
      const stateResource: StateResource<Resource> = createStateResource(createDummyResource());

      beforeEach(() => {
        service.configResource = configResource;
      });

      it('should call shouldLoadResource', () => {
        service.shouldLoadResource = jest.fn();

        service.handleResourceChanges(stateResource, configResource);

        expect(service.shouldLoadResource).toHaveBeenCalledWith(stateResource, configResource);
      });

      it('should load resource', () => {
        service.shouldLoadResource = jest.fn().mockReturnValue(true);
        service.loadResource = jest.fn();

        service.handleResourceChanges(stateResource, configResource);

        expect(service.loadResource).toHaveBeenCalledWith(configResource);
      });

      it('should NOT load resource', () => {
        service.loadResource = jest.fn();
        service.shouldLoadResource = jest.fn().mockReturnValue(false);

        service.handleResourceChanges(stateResource, configResource);

        expect(service.loadResource).not.toHaveBeenCalled();
      });
    });
  });

  describe('handle config resource changes', () => {
    const stateResource: StateResource<Resource> = createStateResource(createDummyResource());

    it('should update configresource', () => {
      service.configResource = createDummyResource();

      service.handleResourceChanges(stateResource, configResource);

      expect(service.configResource).toBe(configResource);
    });

    describe('on stable stateresource', () => {
      beforeEach(() => {
        jest.spyOn(ResourceUtil, 'isStateResoureStable').mockReturnValue(true);
        service.updateStateResourceByConfigResource = jest.fn();
      });

      it('should update stateresource by configresource', () => {
        service.handleResourceChanges(stateResource, configResource);

        expect(service.updateStateResourceByConfigResource).toHaveBeenCalledWith(
          stateResource,
          configResource,
        );
      });
    });

    describe('on instable stateresource', () => {
      beforeEach(() => {
        jest.spyOn(ResourceUtil, 'isStateResoureStable').mockReturnValue(false);
      });

      it('should NOT update stateresource by configresource', () => {
        service.updateStateResourceByConfigResource = jest.fn();

        service.handleResourceChanges(stateResource, configResource);

        expect(service.updateStateResourceByConfigResource).not.toHaveBeenCalled();
      });
    });
  });

  describe('update stateresource by configresource', () => {
    it('should check if should clear stateresource', () => {
      service.shouldClearStateResource = jest.fn();

      service.updateStateResourceByConfigResource(dummyStateResource, configResource);

      expect(service.shouldClearStateResource).toHaveBeenCalled();
    });

    it('should clear resource if should', () => {
      service.stateResource.next(createStateResource(createDummyResource()));
      service.shouldClearStateResource = jest.fn().mockReturnValue(true);

      service.updateStateResourceByConfigResource(dummyStateResource, configResource);

      expect(service.stateResource.value).toEqual(createEmptyStateResource());
    });

    describe('on NOT clearing stateresource', () => {
      beforeEach(() => {
        service.shouldClearStateResource = jest.fn().mockReturnValue(false);
      });

      it('should load resource if link exists', () => {
        service.hasGetLink = jest.fn();

        service.updateStateResourceByConfigResource(dummyStateResource, configResource);

        expect(service.hasGetLink).toHaveBeenCalledWith(configResource);
      });
    });
  });

  describe('should clear stateresource', () => {
    describe('on existing stateresource', () => {
      beforeEach(() => {
        service.stateResource.next(dummyStateResource);
      });

      it('should return true if configresource is null', () => {
        const shouldClear: boolean = service.shouldClearStateResource(dummyStateResource, null);

        expect(shouldClear).toBeTruthy();
      });

      it('should return true if configresource has no get link', () => {
        const shouldClear: boolean = service.shouldClearStateResource(
          dummyStateResource,
          createDummyResource(),
        );

        expect(shouldClear).toBeTruthy();
      });
    });

    describe('on empty stateresource', () => {
      it('should return false', () => {
        const shouldClear: boolean = service.shouldClearStateResource(
          createEmptyStateResource(),
          null,
        );

        expect(shouldClear).toBeFalsy();
      });

      it('should return false if configresource has no get link', () => {
        const shouldClear: boolean = service.shouldClearStateResource(
          createEmptyStateResource(),
          createDummyResource(),
        );

        expect(shouldClear).toBeFalsy();
      });
    });
  });

  describe('should load resource', () => {
    const resource: Resource = createDummyResource();
    const stateResource: StateResource<Resource> = createStateResource(resource);

    let isLoadingRequiredSpy: jest.SpyInstance<boolean>;

    beforeEach(() => {
      isLoadingRequiredSpy = jest.spyOn(ResourceUtil, 'isLoadingRequired');
    });

    it('should return true on existing configresource and loading is required', () => {
      isLoadingRequiredSpy.mockReturnValue(true);

      const shouldLoad: boolean = service.shouldLoadResource(stateResource, configResource);

      expect(shouldLoad).toBeTruthy();
    });

    it('should call isLoadingRequired', () => {
      service.shouldLoadResource(stateResource, configResource);

      expect(isLoadingRequiredSpy).toBeCalledWith(stateResource);
    });

    it('should return false if configresource exists but loading is NOT required', () => {
      isLoadingRequiredSpy.mockReturnValue(false);

      const shouldLoad: boolean = service.shouldLoadResource(stateResource, configResource);

      expect(shouldLoad).toBeFalsy();
    });
  });

  describe('load resource', () => {
    const configResourceWithGetLinkRel: Resource = createDummyResource([getLinkRel]);

    it('should call do load resource', () => {
      service.doLoadResource = jest.fn();

      service.loadResource(configResourceWithGetLinkRel);

      expect(service.doLoadResource).toHaveBeenCalledWith(
        getUrl(configResourceWithGetLinkRel, config.getLinkRel),
      );
    });
  });

  describe('set state resource loading', () => {
    it('should set loading true', () => {
      service.stateResource.next(createStateResource(createDummyResource()));

      service.setStateResourceLoading();

      expect(service.stateResource.value.loading).toBeTruthy();
    });

    it('should set reload false', () => {
      service.stateResource.next({ ...createStateResource(createDummyResource()), reload: true });

      service.setStateResourceLoading();

      expect(service.stateResource.value.reload).toBeFalsy();
    });
  });

  describe('do load resource', () => {
    let resourceUri: ResourceUri;
    let loadedResource: Resource;

    beforeEach(() => {
      service.setStateResourceLoading = jest.fn();
      resourceUri = faker.internet.url();
      loadedResource = createDummyResource();
      repository.getResource.mockReturnValue(of(loadedResource));
    });

    it('should set state resource to loading', () => {
      service.doLoadResource(resourceUri);

      expect(service.setStateResourceLoading).toHaveBeenCalled();
    });

    it('should call repository', () => {
      service.doLoadResource(resourceUri);

      expect(repository.getResource).toHaveBeenCalledWith(resourceUri);
    });

    it('should update stateresource', () => {
      service.updateStateResource = jest.fn();

      service.doLoadResource(resourceUri);

      expect(service.updateStateResource).toHaveBeenCalledWith(loadedResource);
    });
  });

  describe('loadByResourceUri', () => {
    it('should do load resource', () => {
      service.doLoadResource = jest.fn();
      const resourceUri: ResourceUri = faker.internet.url();

      service.loadByResourceUri(resourceUri);

      expect(service.doLoadResource).toHaveBeenCalledWith(resourceUri);
    });
  });

  describe('update stateresource', () => {
    const resourceToBeSet: Resource = createDummyResource();

    it('should set resource as stateresource', () => {
      service.stateResource.next(createEmptyStateResource());

      service.updateStateResource(resourceToBeSet);

      expect(service.stateResource.value).toEqual(createStateResource(resourceToBeSet));
    });
  });

  describe('save', () => {
    const dummyToSave: unknown = {};
    const loadedResource: Resource = createDummyResource();

    const resourceWithEditLinkRel: Resource = createDummyResource([editLinkRel]);

    it('should do save', fakeAsync(() => {
      const stateResource: StateResource<Resource> = createStateResource(resourceWithEditLinkRel);
      service.stateResource.next(stateResource);
      const doSaveMock: jest.Mock = (service.doSave = jest.fn()).mockReturnValue(
        of(loadedResource),
      );

      service.save(dummyToSave).subscribe();
      tick();

      expect(doSaveMock).toHaveBeenCalledWith(resourceWithEditLinkRel, dummyToSave);
    }));

    it('should return saved object', () => {
      service.stateResource.next(createStateResource(resourceWithEditLinkRel));
      service.doSave = jest.fn().mockReturnValue(singleHot(loadedResource));

      const saved: Observable<StateResource<Resource | HttpError>> = service.save(dummyToSave);

      expect(saved).toBeObservable(singleCold(createStateResource(loadedResource)));
    });

    it('should call handleError', () => {
      service.stateResource.next(createStateResource(createDummyResource([config.edit.linkRel])));
      const errorResponse: ProblemDetail = createProblemDetail();
      service.doSave = jest.fn().mockReturnValue(throwError(() => errorResponse));
      service.handleError = jest.fn();

      service.save(<any>{}).subscribe();

      expect(service.handleError).toHaveBeenCalledWith(errorResponse);
    });

    it('should update state resource subject', fakeAsync(() => {
      service.stateResource.next(createStateResource(resourceWithEditLinkRel));
      service.doSave = jest.fn().mockReturnValue(of(loadedResource));

      service.save(dummyToSave).subscribe();
      tick();

      expect(service.stateResource.value).toEqual(createStateResource(loadedResource));
    }));
  });

  describe('handleError', () => {
    it('should return error stateresource on problem unprocessable entity', (done: jest.DoneCallback) => {
      const error: ProblemDetail = createProblemDetail();

      service
        .handleError(<HttpErrorResponse>(<any>error))
        .subscribe((responseError: StateResource<HttpError>) => {
          expect(responseError).toEqual(createErrorStateResource(error));
          done();
        });
    });

    it('should rethrow error', () => {
      const error: HttpErrorResponse = <HttpErrorResponse>{
        status: 500,
        statusText: 'Internal Server Error',
      };

      const thrownError$: Observable<StateResource<HttpError>> = service.handleError(error);

      expect.assertions(1);
      expect(lastValueFrom(thrownError$)).rejects.toThrowError('Internal Server Error');
    });
  });

  describe('refresh', () => {
    beforeEach(() => {
      service.loadResource = jest.fn();
    });

    it('should set reload true on statresource', () => {
      service.stateResource.next(createStateResource(createDummyResource()));

      service.refresh();

      expect(service.stateResource.value.reload).toBeTruthy();
    });
  });

  describe('exist resource', () => {
    it('should return true on existing resource', () => {
      service.stateResource.next(createStateResource(createDummyResource()));

      const existResource$: Observable<boolean> = service.existResource();

      expect(existResource$).toBeObservable(singleCold(true));
    });

    it('should return false on null resource', () => {
      service.stateResource.next(createEmptyStateResource());

      const existResource$: Observable<boolean> = service.existResource();

      expect(existResource$).toBeObservable(singleCold(false));
    });
  });

  describe('select resource', () => {
    it('should return state resource', () => {
      service.stateResource.next(dummyStateResource);

      const resource$: Observable<StateResource<Resource>> = service.selectResource();

      expect(resource$).toBeObservable(singleCold(dummyStateResource));
    });
  });
});

export class DummyResourceService<B extends Resource, T extends Resource> extends ResourceService<
  B,
  T
> {
  constructor(
    protected config: ResourceServiceConfig<B>,
    protected repository: ResourceRepository,
  ) {
    super(config, repository);
  }

  doSave(resource: T, toSave: unknown): Observable<T> {
    return of(resource);
  }
}