/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Events, MessageBus} from 'protocol';
import {FrameManager} from './frame_manager';
import {TestBed} from '@angular/core/testing';
import {Frame} from './application-environment';

describe('FrameManager', () => {
  let frameManager: FrameManager;
  let messageBus: MessageBus<Events>;
  let topicToCallback: {[topic: string]: Function | null};

  function getFrameFromFrameManager(frameId: number): Frame | undefined {
    return frameManager.frames.find((f: Frame) => f.id === frameId);
  }

  function frameConnected(frameId: number): void {
    topicToCallback['frameConnected']!(frameId);
  }

  function contentScriptConnected(frameId: number, name: string, url: string): void {
    topicToCallback['contentScriptConnected']!(frameId, name, url);
  }

  function contentScriptDisconnected(frameId: number): void {
    topicToCallback['contentScriptDisconnected']!(frameId);
  }

  const topLevelFrameId = 0;
  const otherFrameId = 1;
  const tabId = 123;

  beforeEach(() => {
    topicToCallback = {
      frameConnected: null,
      contentScriptConnected: null,
      contentScriptDisconnected: null,
    };
    messageBus = jasmine.createSpyObj('MessageBus', ['on', 'emit']);

    (messageBus.on as any).and.callFake((topic: string, cb: Function) => {
      topicToCallback[topic] = cb;
    });

    (messageBus.emit as any).and.callFake((topic: string, args: any[]) => {
      if (topic === 'enableFrameConnection') {
        frameConnected(args[0]);
      }
    });

    const testModule = TestBed.configureTestingModule({
      providers: [
        {provide: MessageBus, useValue: messageBus},
        {provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
      ],
    });

    frameManager = testModule.inject(FrameManager);
  });

  it('should add frame when contentScriptConnected event is emitted', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    expect(frameManager.frames.length).toBe(1);
    expect(frameManager.frames[0].id).toBe(topLevelFrameId);
    expect(frameManager.frames[0].name).toBe('name');
    expect(frameManager.frames[0].url.toString()).toBe('http://localhost:4200/url');
  });

  it('should set the selected frame to the first frame when there is only one frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
  });

  it('should set selected frame when frameConnected event is emitted', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    frameConnected(otherFrameId);
    expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
  });

  it('should remove frame when contentScriptDisconnected event is emitted', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    expect(frameManager.frames.length).toBe(2);
    contentScriptDisconnected(otherFrameId);
    expect(frameManager.frames.length).toBe(1);
    expect(frameManager.frames[0].id).toBe(topLevelFrameId);

    const errorSpy = spyOn(console, 'error');
    contentScriptDisconnected(topLevelFrameId);
    expect(frameManager.frames.length).toBe(0);
    expect(errorSpy).toHaveBeenCalledWith('Angular DevTools is not connected to any frames.');
  });

  it('should set selected frame to top level frame when contentScriptDisconnected event is emitted for selected frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    frameConnected(otherFrameId);
    expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
    contentScriptDisconnected(otherFrameId);
    expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
  });

  it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non selected frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    frameConnected(topLevelFrameId);
    expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
    contentScriptDisconnected(otherFrameId);
    expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
  });

  it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non existing frame', () => {
    const nonExistingFrameId = 3;

    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    frameConnected(otherFrameId);
    expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
    contentScriptDisconnected(nonExistingFrameId);
    expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
  });

  it('isSelectedFrame should return true when frame matches selected frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
    const otherFrame = getFrameFromFrameManager(otherFrameId);
    expect(topLevelFrame).toBeDefined();
    expect(otherFrame).toBeDefined();
    expect(frameManager.isSelectedFrame(topLevelFrame!)).toBe(true);
  });

  it('isSelectedFrame should return false when frame does not match selected frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
    const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
    const otherFrame = getFrameFromFrameManager(otherFrameId);
    expect(topLevelFrame).toBeDefined();
    expect(otherFrame).toBeDefined();
    expect(frameManager.isSelectedFrame(otherFrame!)).toBe(false);
  });

  it('inspectFrame should emit enableFrameConnection message', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
    expect(topLevelFrame).toBeDefined();
    frameManager.inspectFrame(topLevelFrame!);
    expect(messageBus.emit).toHaveBeenCalledWith('enableFrameConnection', [topLevelFrameId, tabId]);
  });

  it('inspectFrame should set selected frame', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
    contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
    const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
    expect(topLevelFrame).toBeDefined();
    frameManager.inspectFrame(topLevelFrame!);
    expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
  });

  it('frameHasUniqueUrl should return false when a two frames have the same url', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
    contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
    expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
    expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
  });

  it('frameHasUniqueUrl should return true when only one frame has a given url', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
    contentScriptConnected(otherFrameId, 'name', 'https://angular.dev/overview');
    expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
    expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(true);
  });

  it('frameHasUniqueUrl should not consider url fragments as part of the url comparison', () => {
    contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/guide/components');
    contentScriptConnected(
      otherFrameId,
      'name',
      'https://angular.dev/guide/components#using-components',
    );
    expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/guide/components');
    expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
  });

  it('frameHasUniqueUrl should return false when frame is null', () => {
    expect(frameManager.frameHasUniqueUrl(null)).toBe(false);
  });
});
