/*
// Copyright 2024 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
*/
import { options, render } from 'preact';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { useState } from '../../src/index';
import { initGlobalSnapshotPatch, takeGlobalSnapshotPatch } from '../../src/snapshot/lifecycle/patch/snapshotPatch';
import { snapshotPatchApply } from '../../src/snapshot/lifecycle/patch/snapshotPatchApply';
import { setupPage, snapshotInstanceManager, hydrate, backgroundSnapshotInstanceManager } from '../../src/snapshot';
import { transformSpread } from '../../src/snapshot/snapshot/spread';
import { globalEnvManager } from './utils/envManager';
import { elementTree } from './utils/nativeMethod';

let scratch;
let scratchBackground;

beforeAll(() => {
  setupPage(__CreatePage('0', 0));
});

beforeEach(() => {
  globalEnvManager.switchToMainThread();
  scratch = document.createElement('root');
  scratch.ensureElements();
  globalEnvManager.switchToBackground();
  scratchBackground = document.createElement('root');
});

afterEach(() => {
  vi.restoreAllMocks();

  globalEnvManager.switchToMainThread();
  render(null, scratch);
  globalEnvManager.switchToBackground();
  render(null, scratchBackground);

  elementTree.clear();
  backgroundSnapshotInstanceManager.clear();
  backgroundSnapshotInstanceManager.nextId = 0;
  snapshotInstanceManager.clear();
  snapshotInstanceManager.nextId = 0;
  lynx.__runtime_configs__ = { transformBuiltinAttributeNames: false };
});

describe('spreadUpdate', () => {
  it('transforms builtin attribute names and preserves event handler lookup keys', () => {
    lynx.__runtime_configs__ = { transformBuiltinAttributeNames: true };

    expect(
      transformSpread(
        { __id: 7 },
        2,
        {
          __spread: true,
          textMaxline: 2,
          tailColorConvert: false,
          onClick: vi.fn(),
          onCatchTap: vi.fn(),
          onReady: null,
          bindchange: vi.fn(),
        },
      ),
    ).toEqual({
      'text-maxline': 2,
      'tail-color-convert': false,
      bindtap: '7:2:onClick',
      catchtap: '7:2:onCatchTap',
      bindready: null,
      bindchange: '7:2:bindchange',
    });
  });

  it('transforms only after identifying special spread attributes', () => {
    lynx.__runtime_configs__ = {
      transformBuiltinAttributeNames: {
        rename: {
          className: 'renamed-class',
          ref: 'renamed-ref',
          textMaxline: 'custom-maxline',
        },
      },
    };

    expect(
      transformSpread(
        { __id: 8 },
        3,
        {
          __spread: true,
          className: 'primary',
          ref: null,
          textMaxline: 2,
        },
      ),
    ).toEqual({
      className: 'primary',
      ref: undefined,
      'custom-maxline': 2,
    });
  });

  it('renders camel-case spread attributes with transformed element state', () => {
    lynx.__runtime_configs__ = { transformBuiltinAttributeNames: true };

    function Comp() {
      const attributes = {
        textMaxline: '2',
        tailColorConvert: false,
      };
      return <text {...attributes}>1</text>;
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);

    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <text
          tail-color-convert={false}
          text-maxline="2"
        >
          <raw-text
            text="1"
          />
        </text>
      </page>
    `);
  });

  it('basic', async function() {
    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
        className: 'className_str',
        style: 'style_str',
        name: 'name_str',
        class: 'class_str',
      });
      return (
        <view>
          <text {...spread} data-outside={'outside'}>
            1
          </text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class="class_str"
            dataset={
              {
                "outside": "outside",
              }
            }
            flatten={false}
            id="id_str"
            name="name_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('insert', async function() {
    let patch;
    let setSpread_;
    function Comp() {
      const [spread, setSpread] = useState({});
      setSpread_ = setSpread;
      return (
        <view>
          <text {...spread}>1</text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);
    patch = hydrate(JSON.parse(JSON.stringify(scratch)), scratchBackground);
    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();

    setSpread_({
      id: 'id_str',
      className: undefined,
      style: 'style_str',
      'data-a': 'a-a-a',
    });
    render(<Comp />, scratchBackground);
    patch = takeGlobalSnapshotPatch();
    expect(patch).toMatchInlineSnapshot(`
      [
        3,
        -2,
        0,
        {
          "className": "",
          "data-a": "a-a-a",
          "id": "id_str",
          "style": "style_str",
        },
      ]
    `);

    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class=""
            dataset={
              {
                "a": "a-a-a",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('update', async function() {
    let patch;
    let setSpread_;
    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
        className: 'class_str',
        style: 'style_str',
        'data-a': 'a-a-a',
        'data-c': 'c-c-c',
        autoplay: false,
      });
      setSpread_ = setSpread;
      return (
        <view>
          <text {...spread}>1</text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            autoplay={false}
            class="class_str"
            dataset={
              {
                "a": "a-a-a",
                "c": "c-c-c",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);
    patch = hydrate(JSON.parse(JSON.stringify(scratch)), scratchBackground);
    // this update could be removed later
    expect(patch).toMatchInlineSnapshot(`[]`);
    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();
    setSpread_({
      id: 'id_str_2',
      'data-b': 'b-b-b',
      'data-c': 'c-c-c',
      autoplay: true,
    });
    render(<Comp />, scratchBackground);
    patch = takeGlobalSnapshotPatch();
    expect(patch).toMatchInlineSnapshot(`
      [
        3,
        -2,
        0,
        {
          "autoplay": true,
          "data-b": "b-b-b",
          "data-c": "c-c-c",
          "id": "id_str_2",
        },
      ]
    `);

    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            autoplay={true}
            class=""
            dataset={
              {
                "b": "b-b-b",
                "c": "c-c-c",
              }
            }
            id="id_str_2"
            style=""
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('remove', async function() {
    let patch;
    let setSpread_;
    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
        className: 'class_str',
        style: 'style_str',
        'data-a': 'a-a-a',
        autoplay: false,
      });
      setSpread_ = setSpread;
      return (
        <view>
          <text {...spread}>1</text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            autoplay={false}
            class="class_str"
            dataset={
              {
                "a": "a-a-a",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);
    patch = hydrate(JSON.parse(JSON.stringify(scratch)), scratchBackground);
    // this update could be removed later
    expect(patch).toMatchInlineSnapshot(`[]`);
    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();
    setSpread_({});
    render(<Comp />, scratchBackground);
    patch = takeGlobalSnapshotPatch();
    expect(patch).toMatchInlineSnapshot(`
      [
        3,
        -2,
        0,
        {},
      ]
    `);

    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class=""
            dataset={{}}
            id={null}
            style=""
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('remove - null spread', async function() {
    let patch;
    let setSpread_;
    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
        className: 'class_str',
        style: 'style_str',
        'data-a': 'a-a-a',
      });
      setSpread_ = setSpread;
      return (
        <view>
          <text {...spread}>1</text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class="class_str"
            dataset={
              {
                "a": "a-a-a",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);
    patch = hydrate(JSON.parse(JSON.stringify(scratch)), scratchBackground);
    // this update could be removed later
    expect(patch).toMatchInlineSnapshot(`[]`);
    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();
    setSpread_(null);
    render(<Comp />, scratchBackground);
    patch = takeGlobalSnapshotPatch();
    expect(patch).toMatchInlineSnapshot(`
      [
        3,
        -2,
        0,
        {},
      ]
    `);

    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class=""
            dataset={{}}
            id={null}
            style=""
          >
            <raw-text
              text="1"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('multiple spreads', async function() {
    let patch;
    let setSpread_;
    let setSpread2_;
    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
        className: 'class_str',
        style: 'style_str',
        'data-a': 'a-a-a',
      });
      const [spread2, setSpread2] = useState({
        id: 'id_str_2',
        className: 'class_str_2',
        style: 'style_str_2',
        'data-x': 'x-x-x',
      });
      setSpread_ = setSpread;
      setSpread2_ = setSpread2;
      return (
        <view>
          <text {...spread}>1</text>
          <text {...spread}>2</text>
          <text {...spread2}>3</text>
        </view>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class="class_str"
            dataset={
              {
                "a": "a-a-a",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="1"
            />
          </text>
          <text
            class="class_str"
            dataset={
              {
                "a": "a-a-a",
              }
            }
            id="id_str"
            style="style_str"
          >
            <raw-text
              text="2"
            />
          </text>
          <text
            class="class_str_2"
            dataset={
              {
                "x": "x-x-x",
              }
            }
            id="id_str_2"
            style="style_str_2"
          >
            <raw-text
              text="3"
            />
          </text>
        </view>
      </page>
    `);
    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);
    patch = hydrate(JSON.parse(JSON.stringify(scratch)), scratchBackground);
    // this update could be removed later
    expect(patch).toMatchInlineSnapshot(`[]`);
    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();
    setSpread_({});
    render(<Comp />, scratchBackground);
    patch = takeGlobalSnapshotPatch();
    expect(patch).toMatchInlineSnapshot(`
      [
        3,
        -2,
        0,
        {},
        3,
        -2,
        1,
        {},
      ]
    `);

    globalEnvManager.switchToMainThread();
    snapshotPatchApply(patch, 42, false);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text
            class=""
            dataset={{}}
            id={null}
            style=""
          >
            <raw-text
              text="1"
            />
          </text>
          <text
            class=""
            dataset={{}}
            id={null}
            style=""
          >
            <raw-text
              text="2"
            />
          </text>
          <text
            class="class_str_2"
            dataset={
              {
                "x": "x-x-x",
              }
            }
            id="id_str_2"
            style="style_str_2"
          >
            <raw-text
              text="3"
            />
          </text>
        </view>
      </page>
    `);
  });

  it.each(['root', 'scheduled'])('circular reference during %s render', async (renderMode) => {
    await import('../../src/lynx');
    let patch;
    let setSpread_;

    const a = {};
    a.a = a;

    function Foo(props) {
      return props.children;
    }

    function Bar() {
      const [spread, setSpread] = useState({});
      setSpread_ = setSpread;
      return <text {...spread}>1</text>;
    }

    function Comp() {
      return (
        <Foo>
          <Bar />
        </Foo>
      );
    }

    globalEnvManager.switchToMainThread();
    render(<Comp />, scratch);

    globalEnvManager.switchToBackground();
    render(<Comp />, scratchBackground);

    initGlobalSnapshotPatch();
    const previousDebounce = options.debounceRendering;
    let flushScheduledRender;
    options.debounceRendering = callback => {
      flushScheduledRender = callback;
    };
    try {
      setSpread_(a);

      expect(() => {
        if (renderMode === 'scheduled') {
          flushScheduledRender();
        } else {
          render(<Comp />, scratchBackground);
        }
      }).toThrowError(/Converting circular structure to JSON[\s\S]*in Bar\n  in Comp\n$/);
      patch = takeGlobalSnapshotPatch();
      expect(patch).toEqual([]);
    } finally {
      options.debounceRendering = previousDebounce;
    }
  });

  it('should remove __self and __source when spreading props onto element', async function() {
    let componentProps;
    function Child(props) {
      componentProps = JSON.stringify(props);
      return <text {...props}>1</text>;
    }

    function Comp() {
      const [spread, setSpread] = useState({
        id: 'id_str',
      });
      return (
        <view>
          <Child {...spread} key='key' data-outside={'outside'} __self={'self'} __source={'source'} />
        </view>
      );
    }

    globalEnvManager.switchToBackground();
    initGlobalSnapshotPatch();
    render(<Comp />, scratchBackground);

    const patchStr = JSON.stringify(takeGlobalSnapshotPatch());
    expect(patchStr).not.toContain('source');
    expect(patchStr).not.toContain('self');
    expect(patchStr).toContain('outside');
    expect(componentProps).toContain('source');
    expect(componentProps).toContain('self');
    expect(componentProps).toContain('outside');
  });
});
