// 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 * as Platform from '../../core/platform/platform.js';
import * as Protocol from '../../generated/protocol.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {expectCalled} from '../../testing/ExpectStubCall.js';
import {
  describeWithMockConnection,
  setMockConnectionResponseHandler,
} from '../../testing/MockConnection.js';
import {createNetworkRequest} from '../../testing/MockNetworkLog.js';
import {addChildFrame, createResource, DOMAIN, getMainFrame, navigate} from '../../testing/ResourceTreeHelpers.js';

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

const {urlString} = Platform.DevToolsPath;
const MAIN_FRAME_RESOURCE_DOMAIN = urlString`example.org`;
const CHILD_FRAME_RESOURCE_DOMAIN = urlString`example.net`;

describeWithMockConnection('CookieModel', () => {
  const PROTOCOL_COOKIE = {
    domain: '.example.com',
    name: 'name',
    path: '/test',
    size: 23,
    value: 'value',
    expires: 42,
    httpOnly: false,
    secure: false,
    session: true,
    sameParty: false,
    priority: Protocol.Network.CookiePriority.Medium,
    sourcePort: 80,
    sourceScheme: Protocol.Network.CookieSourceScheme.NonSecure,
    partitionKey: undefined,
  };

  const PROTOCOL_COOKIE_PARTITIONED = {
    domain: '.example.com',
    name: 'name',
    path: '/test',
    size: 23,
    value: 'value',
    expires: 42,
    httpOnly: false,
    secure: false,
    session: true,
    sameParty: false,
    priority: Protocol.Network.CookiePriority.Medium,
    sourcePort: 80,
    sourceScheme: Protocol.Network.CookieSourceScheme.NonSecure,
    partitionKey: {topLevelSite: 'https://example.net', hasCrossSiteAncestor: false},
  };

  it('can retrieve cookies for domain', async () => {
    // CDP Connection mock: for Network.getCookies, respond with a single cookie.
    setMockConnectionResponseHandler('Network.getCookies', ({urls}) => {
      return {
        cookies: [
          {...PROTOCOL_COOKIE_PARTITIONED, domain: `.${new URL(urls[0]).host}`},
        ],
      };
    });

    const target = createTarget();
    const mainFrame = getMainFrame(target);
    const resourceUrl = (domain: string) => urlString`${`https://${domain}/resource`}`;
    createResource(mainFrame, resourceUrl(MAIN_FRAME_RESOURCE_DOMAIN), 'text/html', '');
    const childFrame = await addChildFrame(target);
    createResource(childFrame, resourceUrl(CHILD_FRAME_RESOURCE_DOMAIN), 'text/html', '');

    const model = target.model(SDK.CookieModel.CookieModel)!;
    for (const domain of [DOMAIN, MAIN_FRAME_RESOURCE_DOMAIN, CHILD_FRAME_RESOURCE_DOMAIN]) {
      const cookies = await model.getCookiesForDomain(`https://${domain}`);
      assert.isArray(cookies);
      assert.lengthOf(cookies, 1);
      assert.strictEqual(cookies[0].domain(), `.${domain}`);
      assert.strictEqual(cookies[0].name(), 'name');
      assert.strictEqual(cookies[0].path(), '/test');
      assert.strictEqual(cookies[0].size(), 23);
      assert.strictEqual(cookies[0].value(), 'value');
      assert.strictEqual(cookies[0].expires(), 42000);
      assert.isFalse(cookies[0].httpOnly());
      assert.isFalse(cookies[0].secure());
      assert.strictEqual(cookies[0].priority(), Protocol.Network.CookiePriority.Medium);
      assert.strictEqual(cookies[0].sourcePort(), 80);
      assert.strictEqual(cookies[0].sourceScheme(), Protocol.Network.CookieSourceScheme.NonSecure);
      assert.strictEqual(cookies[0].partitionKey().topLevelSite, 'https://example.net');
      assert.isFalse(cookies[0].partitionKey().hasCrossSiteAncestor);
    }
  });

  it('can detect cookie list changes', async () => {
    setMockConnectionResponseHandler('Network.getCookies', ({urls}) => {
      return {
        cookies: [
          {...PROTOCOL_COOKIE, domain: `.${new URL(urls[0]).host}`},
        ],
      };
    });

    const target = createTarget();

    const dispatchLoadingFinished = () => target.model(SDK.NetworkManager.NetworkManager)!.dispatchEventToListeners(
        SDK.NetworkManager.Events.LoadingFinished, createNetworkRequest('1'));

    const mainFrame = getMainFrame(target);
    const model = target.model(SDK.CookieModel.CookieModel)!;

    const eventListener = sinon.stub();
    model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, eventListener);

    assert.isEmpty(await model.getCookiesForDomain(`https://${MAIN_FRAME_RESOURCE_DOMAIN}`));

    const resourceUrl = (domain: string) => urlString`${`https://${domain}/main_resource`}`;
    createResource(mainFrame, resourceUrl(MAIN_FRAME_RESOURCE_DOMAIN), 'text/html', '');
    dispatchLoadingFinished();
    await expectCalled(eventListener);
    assert.isNotEmpty(await model.getCookiesForDomain(`https://${MAIN_FRAME_RESOURCE_DOMAIN}`));
    assert.isEmpty(await model.getCookiesForDomain(`https://${CHILD_FRAME_RESOURCE_DOMAIN}`));

    eventListener.resetHistory();
    const childFrame = await addChildFrame(target);
    createResource(childFrame, resourceUrl(CHILD_FRAME_RESOURCE_DOMAIN), 'text/html', '');
    dispatchLoadingFinished();
    await expectCalled(eventListener);
    assert.isNotEmpty(await model.getCookiesForDomain(`https://${CHILD_FRAME_RESOURCE_DOMAIN}`));
  });

  it('can detect cookie value changes', async () => {
    const cookie = {...PROTOCOL_COOKIE};
    setMockConnectionResponseHandler('Network.getCookies', () => ({cookies: [cookie]}));

    const target = createTarget();
    const dispatchLoadingFinished = () => target.model(SDK.NetworkManager.NetworkManager)!.dispatchEventToListeners(
        SDK.NetworkManager.Events.LoadingFinished, createNetworkRequest('1'));

    const mainFrame = getMainFrame(target);
    const model = target.model(SDK.CookieModel.CookieModel)!;

    const eventListener = sinon.stub();
    model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, eventListener);

    createResource(mainFrame, urlString`${`https://${DOMAIN}/main_resource`}`, 'text/html', '');
    dispatchLoadingFinished();

    await expectCalled(eventListener);
    eventListener.resetHistory();

    cookie.value = 'new value';

    dispatchLoadingFinished();
    await expectCalled(eventListener);
  });

  it('does not refetch cookies while listening unless requested', async () => {
    const cookie = {...PROTOCOL_COOKIE};
    setMockConnectionResponseHandler('Network.getCookies', () => ({cookies: [cookie]}));

    const target = createTarget();
    const dispatchLoadingFinished = () => target.model(SDK.NetworkManager.NetworkManager)!.dispatchEventToListeners(
        SDK.NetworkManager.Events.LoadingFinished, createNetworkRequest('1'));

    const mainFrame = getMainFrame(target);
    const model = target.model(SDK.CookieModel.CookieModel)!;

    createResource(mainFrame, urlString`${`https://${DOMAIN}/main_resource`}`, 'text/html', '');
    dispatchLoadingFinished();

    let [readCookie] = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.strictEqual(readCookie.value(), 'value');

    cookie.value = 'new value';

    model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, () => {});

    [readCookie] = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.strictEqual(readCookie.value(), 'value');

    [readCookie] = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.strictEqual(readCookie.value(), 'value');

    [readCookie] = await model.getCookiesForDomain(`https://${DOMAIN}`, true);
    assert.strictEqual(readCookie.value(), 'new value');
  });

  it('clears stored blocked cookies on primary page change', async () => {
    const target = createTarget();
    const cookieModel = new SDK.CookieModel.CookieModel(target);
    const cookie = new SDK.Cookie.Cookie('name', 'value');
    const blockedReason = {
      attribute: null,
      uiString: 'Setting this cookie was blocked due to third-party cookie phaseout. Learn more in the Issues tab.',
    };

    cookieModel.addBlockedCookie(cookie, [blockedReason]);
    const cookieToBlockedReasons = cookieModel.getCookieToBlockedReasonsMap();
    assert.strictEqual(cookieToBlockedReasons.size, 1);
    assert.deepEqual(cookieToBlockedReasons.get(cookie), [blockedReason]);

    navigate(getMainFrame(target));
    assert.strictEqual(cookieModel.getCookieToBlockedReasonsMap().size, 0);
  });

  it('can delete unpartitioned and partitioned cookies', async () => {
    let cookieArray = [PROTOCOL_COOKIE, PROTOCOL_COOKIE_PARTITIONED];

    // CDP Connection mock.
    setMockConnectionResponseHandler('Network.getCookies', () => {
      return {
        cookies: cookieArray,
      };
    });

    // CDP Connection mock: simplified implementation for Network.deleteCookies, which deletes the matching cookie from `cookies`.
    setMockConnectionResponseHandler('Network.deleteCookies', cookieToDelete => {
      cookieArray = cookieArray.filter(cookie => {
        return !(
            cookie.name === cookieToDelete.name && cookie.domain === cookieToDelete.domain &&
            cookie.path === cookieToDelete.path &&
            cookie.partitionKey?.topLevelSite === cookieToDelete.partitionKey?.topLevelSite &&
            cookie.partitionKey?.hasCrossSiteAncestor === cookieToDelete.partitionKey?.hasCrossSiteAncestor);
      });

      const response = {
        getError() {
          return undefined;
        },
      };
      return Promise.resolve(response);
    });

    const target = createTarget();
    const model = new SDK.CookieModel.CookieModel(target);
    const cookies = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.isArray(cookies);
    assert.lengthOf(cookies, 2);

    await model.deleteCookie(SDK.Cookie.Cookie.fromProtocolCookie(PROTOCOL_COOKIE));

    const cookies2 = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.isArray(cookies2);
    assert.lengthOf(cookies2, 1);

    assert.strictEqual(cookies2[0].domain(), '.example.com');
    assert.strictEqual(cookies2[0].name(), 'name');
    assert.strictEqual(cookies2[0].partitionKey().topLevelSite, 'https://example.net');
    assert.isFalse(cookies2[0].partitionKey().hasCrossSiteAncestor);

    await model.deleteCookie(SDK.Cookie.Cookie.fromProtocolCookie(PROTOCOL_COOKIE_PARTITIONED));

    const cookies3 = await model.getCookiesForDomain(`https://${DOMAIN}`);
    assert.isArray(cookies3);
    assert.lengthOf(cookies3, 0);
  });
});
