/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { Params } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, tap } from 'rxjs/operators';
import { LocationService } from '../location.service';
import { LsLocation } from '../model';
import { LsPosition } from '../model/lsPosition';
import { LocationMapService } from './location-map.service';

describe('LocationMapService', () => {
  class LocationServiceMock extends LocationService {
    locations: LsLocation[] = [
      {
        id: '4',
        name: 'Fe710 EG',
        type: 'floor',
        geoCoordinate: <LsPosition>{
          longitude: 9.17055,
          latitude: 48.81445,
          altitude: 270,
        },
        boundary: {
          type: 'Polygon',
          coordinates: [
            [
              [9.17029693722725, 48.8142892815782],
              [9.170848665081524, 48.81481394670166],
              [9.171023815870287, 48.81473790438878],
              [9.170382767915726, 48.814135618511386],
              [9.17029693722725, 48.8142892815782],
            ],
          ],
        },
      },
      {
        id: '5',
        name: 'Fe710 1. OG',
        type: 'floor',
        geoCoordinate: <LsPosition>{
          longitude: 9.17059,
          latitude: 48.81448,
          altitude: 274,
        },
        boundary: {
          type: 'Polygon',
          coordinates: [
            [
              [9.17029693722725, 48.8142892815782],
              [9.170848665081524, 48.81481394670166],
              [9.171023815870287, 48.81473790438878],
              [9.170382767915726, 48.814135618511386],
              [9.17029693722725, 48.8142892815782],
            ],
          ],
        },
      },
    ];
    private readonly _items = new BehaviorSubject<LsLocation[]>([]);

    constructor() {
      super();
      this.items = this._items.asObservable();
    }

    /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
    queryAll(params?: Params): Observable<LsLocation[]> {
      const selectedItems = this.locations;
      this.itemCount = selectedItems.length;
      setTimeout(() => this._items.next(selectedItems));
      return this.items;
    }
  }

  let service: LocationMapService;
  let locationService: LocationServiceMock;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{ provide: LocationService, useClass: LocationServiceMock }, LocationMapService],
    });
    service = TestBed.inject(LocationMapService);
    locationService = TestBed.inject(LocationService) as any;
  });

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

  describe('selection handling', () => {
    it(
      'should have empty selection initially',
      waitForAsync(() => {
        let selection: LsLocation[] = null;
        service.locationsSelected.pipe(first()).subscribe((selected) => (selection = selected));
        expect(selection).toEqual([]);
      })
    );

    it(
      'should emit changed selection',
      waitForAsync(() => {
        const locations = [
          { id: 'a1', name: 'A', type: 'a' },
          { id: 'b1', name: 'B', type: 'b' },
        ];
        let selectionOfPriorSubscription: LsLocation[] = null;
        let emitCount = 0;
        service.locationsSelected
          .pipe(tap(() => emitCount++))
          .subscribe((selected) => (selectionOfPriorSubscription = selected));
        expect(emitCount).toEqual(1);

        service.selectedLocations = locations;

        let selectionOfLaterSubscription: LsLocation[] = null;
        service.locationsSelected.subscribe((selected) => (selectionOfLaterSubscription = selected));

        expect(emitCount).toEqual(2); // initial and changed value
        expect(selectionOfPriorSubscription).toEqual(locations);
        expect(selectionOfLaterSubscription).toEqual(locations);
      })
    );

    it(
      'should not emit unchanged selection',
      waitForAsync(() => {
        const locations = [
          { id: 'a1', name: 'A', type: 'a' },
          { id: 'b1', name: 'B', type: 'b' },
        ];
        let selectionOfSubscription: LsLocation[] = null;
        let emitCount = 0;
        service.locationsSelected
          .pipe(tap(() => emitCount++))
          .subscribe((selected) => (selectionOfSubscription = selected));

        service.selectedLocations = locations;
        service.selectedLocations = locations;
        service.selectedLocations = locations;

        expect(selectionOfSubscription).toEqual(locations);
        expect(emitCount).toEqual(2); // initial and changed value each once
      })
    );

    it(
      'should emit changed selection',
      waitForAsync(() => {
        const locations = [
          { id: 'a1', name: 'A', type: 'a' },
          { id: 'b1', name: 'B', type: 'b' },
        ];
        let selectionOfSubscription: LsLocation[] = null;
        let emitCount = 0;
        service.locationsSelected
          .pipe(tap(() => emitCount++))
          .subscribe((selected) => (selectionOfSubscription = selected));

        service.selectedLocations = [locations[0]];
        service.selectedLocations = locations;

        expect(selectionOfSubscription).toEqual(locations);
        expect(emitCount).toEqual(3); // intial value and the two changes
      })
    );

    it('should offer locations from location service', fakeAsync(() => {
      locationService.queryAll();
      tick();

      let offeredLocations: LsLocation[] = null;
      service.locations.subscribe((locations) => (offeredLocations = locations));

      expect(offeredLocations).toEqual(locationService.locations);
    }));
  });

  describe('location editing', () => {
    it('should merge locations from location service with the location being edited', fakeAsync(() => {
      locationService.queryAll();
      const clonedBeingEdited = Object.assign({}, locationService.locations[0], { name: "I'm a (shallow) clone." });
      service.locationBeingEdited = clonedBeingEdited;
      tick();

      let offeredLocations: LsLocation[] = null;
      service.locations.subscribe((locations) => (offeredLocations = locations));

      expect(offeredLocations).toEqual([locationService.locations[1], clonedBeingEdited]);
    }));

    it('should emit location being edited', fakeAsync(() => {
      let beingEdited = null;
      service.locationBeingEditedObservable.subscribe((l) => (beingEdited = l));

      const toBeEdited = locationService.locations[0];
      service.locationBeingEdited = toBeEdited;
      tick();

      expect(beingEdited).toEqual(toBeEdited);
      expect(beingEdited).toEqual(service.locationBeingEdited);
    }));

    it('should select location being edited', fakeAsync(() => {
      const toBeEdited = locationService.locations[0];
      service.locationBeingEdited = toBeEdited;
      tick();

      expect(service.selectedLocations).toEqual([toBeEdited]);
    }));
  });

  describe('boundary detection', () => {
    it('should detect absent boundary', () => {
      const location = locationService.locations[0];
      delete location.boundary;
      expect(service.hasBoundary(location)).toEqual(false);
    });

    it('should detect absent boundary coordinates', () => {
      const location = locationService.locations[0];
      delete location.boundary.coordinates;
      expect(service.hasBoundary(location)).toEqual(false);
    });

    it('should detect empty boundary coordinates', () => {
      const location = locationService.locations[0];
      location.boundary.coordinates.length = 0;
      expect(service.hasBoundary(location)).toEqual(false);
    });

    it('should detect boundary', () => {
      const location = locationService.locations[0];
      expect(service.hasBoundary(location)).toEqual(true);
    });
  });

  describe('position detection', () => {
    it('should detect absent position', () => {
      const location: Partial<LsLocation> = {};
      expect(service.hasPosition(location)).toEqual(false);
    });

    it('should detect absent position (null|null)', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: null,
          latitude: null,
          altitude: 42,
        },
      };
      expect(service.hasPosition(location)).toEqual(false);
    });

    it('should detect absent position (undefined|undefined)', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: undefined,
          latitude: undefined,
          altitude: 42,
        },
      };
      expect(service.hasPosition(location)).toEqual(false);
    });

    it('should detect given position', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: 12,
          latitude: -2,
          altitude: 42,
        },
      };
      expect(service.hasPosition(location)).toEqual(true);
    });

    it('should detect given position without altitude', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: 12,
          latitude: -2,
        },
      };
      expect(service.hasPosition(location)).toEqual(true);
    });

    it('should detect given position (0|0)', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: 0,
          latitude: 0,
        },
      };
      expect(service.hasPosition(location)).toEqual(true);
    });

    it('should detect given position (0)', () => {
      const location: Partial<LsLocation> = {
        geoCoordinate: {
          longitude: 0,
        },
      };
      expect(service.hasPosition(location)).toEqual(false);
    });
  });
});
