// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {activate, getMainFrame, LOADER_ID, navigate} from '../../testing/ResourceTreeHelpers.js';
import * as Logs from '../logs/logs.js';

const {urlString} = Platform.DevToolsPath;

function url(input: string): Platform.DevToolsPath.UrlString {
  return urlString`${input as unknown}`;
}

describe('NetworkLog', () => {
  describe('initiatorInfoForRequest', () => {
    const {initiatorInfoForRequest} = Logs.NetworkLog.NetworkLog;

    it('uses the passed in initiator info if it exists', () => {
      const request = {
        initiator() {
          return null;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const existingInfo: Logs.NetworkLog.InitiatorData = {
        info: null,
        chain: null,
        request: undefined,
      };
      const info = initiatorInfoForRequest(request, existingInfo);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.OTHER,
        url: Platform.DevToolsPath.EmptyUrlString,
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
      assert.deepEqual(info, existingInfo.info);
    });

    it('returns "other" if there is no initiator or redirect', () => {
      const request = {
        initiator() {
          return null;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.OTHER,
        url: Platform.DevToolsPath.EmptyUrlString,
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the redirect info if the request has a redirect', () => {
      const request = {
        initiator() {
          return null;
        },
        redirectSource() {
          return {
            url() {
              return url('http://localhost:3000/example.js');
            },
          } as unknown as SDK.NetworkRequest.NetworkRequest;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.REDIRECT,
        url: url('http://localhost:3000/example.js'),
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the initiator info if the initiator is the parser', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Parser,
            url: url('http://localhost:3000/example.js'),
            lineNumber: 5,
            columnNumber: 6,
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.PARSER,
        url: url('http://localhost:3000/example.js'),
        lineNumber: 5,
        columnNumber: 6,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the initiator info if the initiator is a script with a stack', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Script,
            url: url('http://localhost:3000/example.js'),
            stack: {
              callFrames: [{
                functionName: 'foo',
                url: url('http://localhost:3000/example.js'),
                scriptId: 'script-id-1' as Protocol.Runtime.ScriptId,
                lineNumber: 5,
                columnNumber: 6,
              }],
            },
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.SCRIPT,
        url: url('http://localhost:3000/example.js'),
        lineNumber: 5,
        columnNumber: 6,
        scriptId: 'script-id-1' as Protocol.Runtime.ScriptId,
        stack: {
          callFrames: [{
            functionName: 'foo',
            url: url('http://localhost:3000/example.js'),
            scriptId: 'script-id-1' as Protocol.Runtime.ScriptId,
            lineNumber: 5,
            columnNumber: 6,
          }],
        },
        initiatorRequest: null,
      });
    });

    it('deals with a nested stack and finds the top frame to use for the script-id', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Script,
            url: url('http://localhost:3000/example.js'),
            stack: {
              parent: {
                callFrames: [{
                  functionName: 'foo',
                  url: url('http://localhost:3000/example.js'),
                  scriptId: 'script-id-1' as Protocol.Runtime.ScriptId,
                  lineNumber: 5,
                  columnNumber: 6,
                }],
              },
              callFrames: [],
            },
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.SCRIPT,
        url: url('http://localhost:3000/example.js'),
        lineNumber: 5,
        columnNumber: 6,
        scriptId: 'script-id-1' as Protocol.Runtime.ScriptId,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the initiator info if the initiator is a script without a stack', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Script,
            url: url('http://localhost:3000/example.js'),
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.SCRIPT,
        url: url('http://localhost:3000/example.js'),
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the info for a Preload request', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Preload,
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.PRELOAD,
        url: Platform.DevToolsPath.EmptyUrlString,
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });

    it('returns the info for a Preflight request', () => {
      const PREFLIGHT_INITIATOR_REQUEST = {} as unknown as SDK.NetworkRequest.NetworkRequest;
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.Preflight,
          } as unknown as Protocol.Network.Initiator;
        },
        preflightInitiatorRequest() {
          return PREFLIGHT_INITIATOR_REQUEST;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.PREFLIGHT,
        url: Platform.DevToolsPath.EmptyUrlString,
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: PREFLIGHT_INITIATOR_REQUEST,
      });
    });

    it('returns the info for a signed exchange request', () => {
      const request = {
        initiator() {
          return {
            type: Protocol.Network.InitiatorType.SignedExchange,
            url: url('http://localhost:3000/example.js'),
          } as unknown as Protocol.Network.Initiator;
        },
        redirectSource() {
          return null;
        },
      } as unknown as SDK.NetworkRequest.NetworkRequest;
      const info = initiatorInfoForRequest(request);
      assert.deepEqual(info, {
        type: SDK.NetworkRequest.InitiatorType.SIGNED_EXCHANGE,
        url: url('http://localhost:3000/example.js'),
        lineNumber: undefined,
        columnNumber: undefined,
        scriptId: null,
        stack: null,
        initiatorRequest: null,
      });
    });
  });
});

describeWithMockConnection('NetworkLog', () => {
  it('clears on main frame navigation', () => {
    const networkLog = Logs.NetworkLog.NetworkLog.instance();
    const tabTarget = createTarget({type: SDK.Target.Type.TAB});
    const mainFrameTarget = createTarget({parentTarget: tabTarget});
    const mainFrame = getMainFrame(mainFrameTarget);
    const subframe = getMainFrame(createTarget({parentTarget: mainFrameTarget}));

    let networkLogResetEvents = 0;
    networkLog.addEventListener(Logs.NetworkLog.Events.Reset, () => ++networkLogResetEvents);

    navigate(subframe);
    assert.strictEqual(networkLogResetEvents, 0);

    navigate(mainFrame);
    assert.strictEqual(networkLogResetEvents, 1);
  });

  describe('on primary page changed', () => {
    let networkLog: Logs.NetworkLog.NetworkLog;
    let target: SDK.Target.Target;

    beforeEach(() => {
      Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log').set(false);
      target = createTarget();
      const networkManager = target.model(SDK.NetworkManager.NetworkManager);
      assert.exists(networkManager);
      networkLog = Logs.NetworkLog.NetworkLog.instance();
      const networkDispatcher = new SDK.NetworkManager.NetworkDispatcher(networkManager);

      const requestWillBeSentEvent1 = {requestId: 'mockId1', request: {url: 'example.com'}, loaderId: LOADER_ID} as
          Protocol.Network.RequestWillBeSentEvent;
      networkDispatcher.requestWillBeSent(requestWillBeSentEvent1);
      const requestWillBeSentEvent2 = {requestId: 'mockId2', request: {url: 'foo.com'}, loaderId: 'OTHER_LOADER_ID'} as
          Protocol.Network.RequestWillBeSentEvent;
      networkDispatcher.requestWillBeSent(requestWillBeSentEvent2);
      assert.lengthOf(networkLog.requests(), 2);
    });

    it('discards requests with mismatched loaderId on navigation', () => {
      navigate(getMainFrame(target));
      assert.deepEqual(networkLog.requests().map(request => request.requestId()), ['mockId1']);
    });

    it('does not discard requests on prerender activation', () => {
      activate(target);
      assert.deepEqual(networkLog.requests().map(request => request.requestId()), ['mockId1', 'mockId2']);
    });
  });

  it('removes preflight requests with a UnexpectedPrivateNetworkAccess CORS error', () => {
    const target = createTarget();
    const networkManager = target.model(SDK.NetworkManager.NetworkManager);
    if (!networkManager) {
      throw new Error('No networkManager');
    }
    const networkLog = Logs.NetworkLog.NetworkLog.instance();
    let removedRequest: SDK.NetworkRequest.NetworkRequest|null = null;
    networkLog.addEventListener(Logs.NetworkLog.Events.RequestRemoved, event => {
      assert.isNull(removedRequest, 'Request was removed multiple times.');
      removedRequest = event.data.request;
    });

    const request = {
      requestId: () => 'request-id',
      isPreflightRequest: () => true,
      initiator: () => null,
      corsErrorStatus: () => ({corsError: Protocol.Network.CorsError.UnexpectedPrivateNetworkAccess}),
    } as SDK.NetworkRequest.NetworkRequest;
    networkManager.dispatchEventToListeners(SDK.NetworkManager.Events.RequestStarted, {request, originalRequest: null});
    assert.lengthOf(networkLog.requests(), 1);

    networkManager.dispatchEventToListeners(SDK.NetworkManager.Events.RequestUpdated, request);
    assert.strictEqual(request, removedRequest);
    assert.lengthOf(networkLog.requests(), 0);
  });
});
