// Copyright 2020 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 type * as Protocol from '../../generated/protocol.js';
import {
  createTarget,
  describeWithEnvironment,
  describeWithLocale,
} from '../../testing/EnvironmentHelpers.js';
import {
  describeWithMockConnection,
  setMockConnectionResponseHandler,
} from '../../testing/MockConnection.js';
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as Platform from '../platform/platform.js';

import * as SDK from './sdk.js';

const {urlString} = Platform.DevToolsPath;

interface LoadResult {
  success: boolean;
  content: string;
  errorDescription: Host.ResourceLoader.LoadErrorDescription;
}

const initiator = {
  target: null,
  frameId: '123' as Protocol.Page.FrameId,
  initiatorUrl: Platform.DevToolsPath.EmptyUrlString,
};

describeWithLocale('PageResourceLoader', () => {
  const foo1Url = urlString`foo1`;
  const foo2Url = urlString`foo2`;
  const foo3Url = urlString`foo3`;
  const loads: Array<{url: string, resolve?: {(_: LoadResult|PromiseLike<LoadResult>): void}}> = [];
  const load = async (url: string) => {
    loads.push({url});

    return {
      success: true,
      content: `${url} - content`,
      errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
    };
  };

  beforeEach(() => {
    loads.length = 0;
  });

  it('registers extension loads', async () => {
    const loader = SDK.PageResourceLoader.PageResourceLoader.instance(
        {forceNew: true, loadOverride: load, maxConcurrentLoads: 500});

    const initiator: SDK.PageResourceLoader.ExtensionInitiator = {
      extensionId: '123',
      initiatorUrl: urlString`www.test.com/main.wasm.dwp`,
      target: null,
      frameId: null,
    };
    const extensionResource: SDK.PageResourceLoader.PageResource = {
      url: urlString`main.wasm.dwp`,
      success: true,
      initiator,
      size: null,
    };

    loader.resourceLoadedThroughExtension(extensionResource);
    assert.deepEqual(loader.getScopedNumberOfResources(), {loading: 0, resources: 1});
    assert.deepEqual(loader.getNumberOfResources(), {loading: 0, queued: 0, resources: 1});

    const resources = Array.from(loader.getResourcesLoaded().values());

    assert.lengthOf(resources, 1);
    assert.isTrue(resources[0].success);
    assert.deepEqual(resources[0].initiator, initiator);
  });

  it('loads resources correctly', async () => {
    const loader = SDK.PageResourceLoader.PageResourceLoader.instance(
        {forceNew: true, loadOverride: load, maxConcurrentLoads: 500});
    const loading = [
      loader.loadResource(foo1Url, initiator),
      loader.loadResource(foo2Url, initiator),
      loader.loadResource(foo3Url, initiator),
    ];

    assert.deepEqual(loader.getNumberOfResources(), {loading: 3, queued: 0, resources: 3});

    const results = await Promise.all(loading);
    assert.deepEqual(loads.map(x => x.url), ['foo1', 'foo2', 'foo3']);
    assert.deepEqual(results.map(x => x.content), ['foo1 - content', 'foo2 - content', 'foo3 - content']);
    assert.deepEqual(loader.getNumberOfResources(), {loading: 0, queued: 0, resources: 3});
    const resources = Array.from(loader.getResourcesLoaded().values());
    assert.isTrue(resources.every(x => x.success));
  });

  it('deals with page reloads correctly', async () => {
    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: load, maxConcurrentLoads: 1});
    const loading = [
      loader.loadResource(foo1Url, initiator).catch(e => e.message),
      loader.loadResource(foo2Url, initiator).catch(e => e.message),
      loader.loadResource(foo3Url, initiator).catch(e => e.message),
    ];
    assert.deepEqual(loader.getNumberOfResources(), {loading: 3, queued: 2, resources: 3});

    loader.onPrimaryPageChanged({
      data: {
        frame: {
          isOutermostFrame() {
            return true;
          },
          resourceTreeModel() {
            return {
              target() {
                return null;
              },
            };
          },
        } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
        type: SDK.ResourceTreeModel.PrimaryPageChangeType.NAVIGATION,
      },
    });
    assert.deepEqual(loader.getNumberOfResources(), {loading: 3, queued: 0, resources: 0});

    const results = await Promise.all(loading);
    assert.deepEqual(loads.map(x => x.url), ['foo1']);
    assert.deepEqual(results[0].content, 'foo1 - content');
    assert.deepEqual(results[1], 'Load canceled due to reload of inspected page');
    assert.deepEqual(results[2], 'Load canceled due to reload of inspected page');
  });

  it('respects the max concurrent loads', async () => {
    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: load, maxConcurrentLoads: 2});
    const loading = [
      loader.loadResource(foo1Url, initiator),
      loader.loadResource(foo2Url, initiator),
      loader.loadResource(foo3Url, initiator),
    ];
    assert.deepEqual(loader.getNumberOfResources(), {loading: 3, queued: 1, resources: 3});

    const results = await Promise.all(loading);
    assert.deepEqual(loads.map(x => x.url), ['foo1', 'foo2', 'foo3']);
    assert.deepEqual(results.map(x => x.content), ['foo1 - content', 'foo2 - content', 'foo3 - content']);
    assert.deepEqual(loader.getNumberOfResources(), {loading: 0, queued: 0, resources: 3});
    const resources = Array.from(loader.getResourcesLoaded().values());
    assert.isTrue(resources.every(x => x.success));
  });
});

// Loading via host bindings requires the settings infra to be booted.
describeWithEnvironment('PageResourceLoader', () => {
  it('blocks UNC file paths with the default setting', async () => {
    if (!Host.Platform.isWin()) {
      return;
    }

    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1});

    const message = await loader.loadResource(urlString`file:////127.0.0.1/share/source-map.js.map`, initiator)
                        .catch(e => e.message);

    assert.include(message, 'remote file');
  });

  it('blocks remote file paths with the default setting', async () => {
    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1});

    const message =
        await loader.loadResource(urlString`file://host/source-map.js.map`, initiator).catch(e => e.message);

    assert.include(message, 'remote file');
  });

  it('blocks UNC file paths with a backslash on Windows with the default setting', async () => {
    if (!Host.Platform.isWin()) {
      return;
    }

    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1});

    const message = await loader.loadResource(urlString`file:///\\127.0.0.1/share/source-map.js.map`, initiator)
                        .catch(e => e.message);

    assert.include(message, 'remote file');
  });

  it('allows remote file paths with the setting enabled', async () => {
    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1});
    sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource')
        .callsFake((_url, _headers, streamId, callback) => {
          Host.ResourceLoader.streamWrite(streamId, 'content of the source map');
          callback({statusCode: 200});
        });

    Common.Settings.Settings.instance().moduleSetting('network.enable-remote-file-loading').set(true);
    const response = await loader.loadResource(urlString`file://host/source-map.js.map`, initiator);

    assert.strictEqual(response.content, 'content of the source map');
  });

  it('allows UNC paths on Windows with the setting enabled', async () => {
    if (!Host.Platform.isWin()) {
      return;
    }

    const loader =
        SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1});
    sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource')
        .callsFake((_url, _headers, streamId, callback) => {
          Host.ResourceLoader.streamWrite(streamId, 'content of the source map');
          callback({statusCode: 200});
        });

    Common.Settings.Settings.instance().moduleSetting('network.enable-remote-file-loading').set(true);
    const response = await loader.loadResource(urlString`file:////127.0.0.1/share/source-map.js.map`, initiator);

    assert.strictEqual(response.content, 'content of the source map');
  });
});

describeWithMockConnection('PageResourceLoader', () => {
  describe('loadResource', () => {
    const stream = 'STREAM_ID' as Protocol.IO.StreamHandle;
    const initiatorUrl = urlString`htp://example.com`;
    const url = urlString`${`${initiatorUrl}/test.txt`}`;

    function setupLoadingSourceMapsAsNetworkResource(): Promise<Protocol.Network.LoadNetworkResourceRequest> {
      return new Promise(resolve => {
        let contentToRead: string|null = 'foo';
        setMockConnectionResponseHandler('IO.read', () => {
          const data = contentToRead;
          contentToRead = null;
          return {data};
        });
        setMockConnectionResponseHandler('IO.close', () => ({}));
        setMockConnectionResponseHandler('Network.loadNetworkResource', request => {
          resolve(request);
          return {resource: {success: true, stream, statusCode: 200}};
        });
      });
    }

    for (const disableCache of [true, false]) {
      it(`loads with ${disableCache ? 'disabled' : 'enabled'} cache based on the setting`, async () => {
        Common.Settings.Settings.instance().moduleSetting('cache-disabled').set(disableCache);
        const target = createTarget();
        const initiator = {target, frameId: null, initiatorUrl};
        const loader = SDK.PageResourceLoader.PageResourceLoader.instance();
        const [{options}, {content}] = await Promise.all([
          setupLoadingSourceMapsAsNetworkResource(),
          loader.loadResource(url, initiator),
        ]);
        // Check that we loaded the resources with appropriately enabled caching.
        assert.strictEqual(options.disableCache, disableCache);
        // Sanity check on the content.
        assert.deepEqual(content, 'foo');
      });
    }
  });
});

describeWithMockConnection('PageResourceLoader', () => {
  const initiatorUrl = urlString`htp://example.com`;
  const foo1Url = urlString`foo1`;
  const foo2Url = urlString`foo2`;
  const foo3Url = urlString`foo3`;

  it('handles scoped resources', async () => {
    const target = createTarget({id: 'main' as Protocol.Target.TargetID});
    const prerenderTarget = createTarget({id: 'prerender' as Protocol.Target.TargetID});
    const initiator = {target, frameId: null, initiatorUrl};
    const prerenderInitiator = {target: prerenderTarget, frameId: null, initiatorUrl};
    const load = async () => {
      await new Promise(() => {});
      return {
        success: true,
        content: 'content',
        errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
      };
    };
    const loader = SDK.PageResourceLoader.PageResourceLoader.instance(
        {forceNew: true, loadOverride: load, maxConcurrentLoads: 500});

    void loader.loadResource(foo1Url, initiator);
    void loader.loadResource(foo2Url, initiator);
    void loader.loadResource(foo3Url, prerenderInitiator);

    assert.deepEqual(loader.getNumberOfResources(), {loading: 3, queued: 0, resources: 3});
    assert.deepEqual(loader.getScopedNumberOfResources(), {loading: 2, resources: 2});

    let resources = loader.getScopedResourcesLoaded();
    let resourceUrls = [...resources.values()].map(x => x.url);
    assert.deepEqual(resourceUrls, [foo1Url, foo2Url]);

    SDK.TargetManager.TargetManager.instance().setScopeTarget(prerenderTarget);
    assert.deepEqual(loader.getScopedNumberOfResources(), {loading: 1, resources: 1});

    resources = loader.getScopedResourcesLoaded();
    resourceUrls = [...resources.values()].map(x => x.url);
    assert.deepEqual(resourceUrls, [foo3Url]);
  });

  it('handles prerender activation', async () => {
    const target = createTarget({id: 'main' as Protocol.Target.TargetID});
    const prerenderTarget = createTarget({id: 'prerender' as Protocol.Target.TargetID});
    const initiator = {target, frameId: null, initiatorUrl};
    const prerenderInitiator = {target: prerenderTarget, frameId: null, initiatorUrl};

    const load = async (url: string) => {
      return {
        success: true,
        content: `${url} - content`,
        errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
      };
    };
    const loader = SDK.PageResourceLoader.PageResourceLoader.instance(
        {forceNew: true, loadOverride: load, maxConcurrentLoads: 500});

    await Promise.all([
      loader.loadResource(foo1Url, initiator),
      loader.loadResource(foo2Url, initiator),
      loader.loadResource(foo3Url, prerenderInitiator),
    ]);

    assert.deepEqual(loader.getNumberOfResources(), {loading: 0, queued: 0, resources: 3});
    assert.deepEqual(loader.getScopedNumberOfResources(), {loading: 0, resources: 2});

    let resources = loader.getScopedResourcesLoaded();
    let resourceUrls = [...resources.values()].map(x => x.url);
    assert.deepEqual(resourceUrls, [foo1Url, foo2Url]);

    loader.onPrimaryPageChanged({
      data: {
        frame: {
          isOutermostFrame() {
            return true;
          },
          resourceTreeModel() {
            return {
              target() {
                return prerenderTarget;
              },
            };
          },
        } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
        type: SDK.ResourceTreeModel.PrimaryPageChangeType.ACTIVATION,
      },
    });
    assert.deepEqual(loader.getNumberOfResources(), {loading: 0, queued: 0, resources: 1});

    SDK.TargetManager.TargetManager.instance().setScopeTarget(prerenderTarget);
    assert.deepEqual(loader.getScopedNumberOfResources(), {loading: 0, resources: 1});

    resources = loader.getScopedResourcesLoaded();
    resourceUrls = [...resources.values()].map(x => x.url);
    assert.deepEqual(resourceUrls, [foo3Url]);
  });
});
