/* eslint-disable @typescript-eslint/unbound-method */
import { TestBed } from '@angular/core/testing';
import { ɵZoneScheduler } from '@angular/fire';
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFireDatabase, AngularFireDatabaseModule, USE_EMULATOR, fromRef } from '@angular/fire/compat/database';
import firebase from 'firebase/compat/app';
import { take } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
import { COMMON_CONFIG, databaseEmulatorPort } from '../../../../src/test-config';
import { rando } from '../../../../src/utils';

describe('fromRef', () => {
  let db: AngularFireDatabase;
  let ref: (path: string) => firebase.database.Reference;
  let batch = {};
  const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }].map(item => ({ key: rando(), ...item }));
  Object.keys(items).forEach((key) => {
    const itemValue = items[key];
    batch[itemValue.key] = itemValue;
  });
  // make batch immutable to preserve integrity
  batch = Object.freeze(batch);

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        AngularFireModule.initializeApp(COMMON_CONFIG, rando()),
        AngularFireDatabaseModule
      ],
      providers: [
        { provide: USE_EMULATOR, useValue: ['localhost', databaseEmulatorPort] }
      ]
    });

    db = TestBed.inject(AngularFireDatabase);
    ref = (path: string) => db.database.ref(path);
  });

  it('it should be async by default', (done) => {
    const itemRef = ref(rando());
    itemRef.set(batch);
    const obs = fromRef(itemRef, 'value');
    let count = 0;
    expect(count).toEqual(0);
    const sub = obs.subscribe(() => {
      count = count + 1;
      expect(count).toEqual(1);
      sub.unsubscribe();
      done();
    });
    expect(count).toEqual(0);
  });

  it('should take a scheduler', done => {
    const itemRef = ref(rando());
    itemRef.set(batch);

    const testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
    spyOn(testScheduler, 'schedule').and.callThrough();

    const obs = fromRef(itemRef, 'value', 'once', testScheduler);
    expect(testScheduler.schedule).not.toHaveBeenCalled();

    obs.subscribe(() => {
      expect(testScheduler.schedule).toHaveBeenCalled();
      // done();
    }, err => {
      console.error(err);
      expect(false).withContext('Shouldnt error').toEqual(true);
      // done();
    }, () => {
      expect(testScheduler.schedule).toHaveBeenCalled();
      done();
    });
    testScheduler.flush();
  });

  it('should schedule completed and error correctly', done => {
    const testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
    spyOn(testScheduler, 'schedule').and.callThrough();

    // Error
    const errorObservable = fromRef({
        once: (event, snap, err) => err()
      } as any,
      'value',
      'once',
      testScheduler
    );
    errorObservable.subscribe(() => {
      fail('Should not emit');
    }, () => {
      expect(testScheduler.schedule).toHaveBeenCalled();
    }, () => {
      fail('Should not complete');
    });

    testScheduler.flush();

    // Completed
    const itemRef = ref(rando());
    itemRef.set(batch);

    const scheduler = new ɵZoneScheduler(Zone.current.fork({
      name: 'ExpectedZone'
    }));
    const completeObservable = fromRef(
      itemRef,
      'value',
      'once',
      scheduler
    );
    completeObservable.subscribe(
      () => undefined,
      () => fail('Should not error'),
      () => expect(Zone.current.name).toEqual('ExpectedZone')
    );
    testScheduler.flush();
    done();
  });


  it('it should should handle non-existence', (done) => {
    const itemRef = ref(rando());
    itemRef.set({});
    const obs = fromRef(itemRef, 'value');
    obs.pipe(take(1)).subscribe(change => {
      expect(change.payload.exists()).toEqual(false);
      expect(change.payload.val()).toEqual(null);
    }).add(done);
  });

  it('once should complete', (done) => {
    const itemRef = ref(rando());
    itemRef.set(batch);
    const obs = fromRef(itemRef, 'value', 'once');
    obs.subscribe(() => undefined, () => undefined, done);
  });

  it('it should listen and then unsubscribe', (done) => {
    const itemRef = ref(rando());
    itemRef.set(batch);
    const obs = fromRef(itemRef, 'value');
    let count = 0;
    const sub = obs.subscribe(() => {
      count = count + 1;
      // hard coding count to one will fail if the unsub
      // doesn't actually unsub
      expect(count).toEqual(1);
      done();
      sub.unsubscribe();
      itemRef.push({ name: 'anotha one' });
    });
  });

  describe('events', () => {

    it('should stream back a child_added event', done => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const obs = fromRef(itemRef, 'child_added');
      let count = 0;
      const sub = obs.subscribe(change => {
        count = count + 1;
        const { type, payload } = change;
        expect(type).toEqual('child_added');
        expect(payload.val()).toEqual(batch[payload.key]);
        if (count === items.length) {
          done();
          sub.unsubscribe();
          expect(sub.closed).toEqual(true);
        }
      });
    });

    it('should stream back a child_changed event', done => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const obs = fromRef(itemRef, 'child_changed');
      const name = 'look at what you made me do';
      const key = items[0].key;
      const sub = obs.subscribe(change => {
        const { type, payload } = change;
        expect(type).toEqual('child_changed');
        expect(payload.key).toEqual(key);
        expect(payload.val()).toEqual({ key, name });
        sub.unsubscribe();
        done();
      });
      itemRef.child(key).update({ name });
    });

    it('should stream back a child_removed event', done => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const obs = fromRef(itemRef, 'child_removed');
      const key = items[0].key;
      const name = items[0].name;
      const sub = obs.subscribe(change => {
        const { type, payload } = change;
        expect(type).toEqual('child_removed');
        expect(payload.key).toEqual(key);
        expect(payload.val()).toEqual({ key, name });
        sub.unsubscribe();
        done();
      });
      itemRef.child(key).remove();
    });

    it('should stream back a child_moved event', done => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const obs = fromRef(itemRef, 'child_moved');
      const key = items[2].key;
      const name = items[2].name;
      const sub = obs.subscribe(change => {
        const { type, payload } = change;
        expect(type).toEqual('child_moved');
        expect(payload.key).toEqual(key);
        expect(payload.val()).toEqual({ key, name });
        sub.unsubscribe();
        done();
      });
      itemRef.child(key).setPriority(-100, () => undefined);
    });

    it('should stream back a value event', (done: any) => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const obs = fromRef(itemRef, 'value');
      const sub = obs.subscribe(change => {
        const { type, payload } = change;
        expect(type).toEqual('value');
        expect(payload.val()).toEqual(batch);
        done();
        sub.unsubscribe();
        expect(sub.closed).toEqual(true);
      });
    });

    it('should stream back query results', (done: any) => {
      const itemRef = ref(rando());
      itemRef.set(batch);
      const query = itemRef.orderByChild('name').equalTo(items[0].name);
      const obs = fromRef(query, 'value');
      obs.subscribe(change => {
        let child = null;
        change.payload.forEach(snap => {
          child = snap.val();
          return true;
        });
        expect(child).toEqual(items[0]);
        done();
      });
    });

  });

});
