import { describe, expect, vi } from 'vitest';
import { fireEvent, render, waitSchedule } from '..';
import {
  Component,
  defineMainThreadObjectType,
  runOnBackground,
  runOnMainThread,
  useMainThreadObject,
  useMainThreadRef,
} from '@lynx-js/react';

expect.addSnapshotSerializer({
  test: value => typeof value === 'string' && /"_execId":\d+/.test(value),
  print: (value, serialize) => serialize(value.replace(/"_execId":\d+/g, '"_execId":"[number]"')),
});

const guardedClassCaptureType = defineMainThreadObjectType({
  type: '@test/guarded-class-capture',
  create: initialValue => ({
    get: () => initialValue,
  }),
});

describe('worklet', () => {
  it('resolves the registration owner when a MIXED module imports the init entry on BTS', async () => {
    expect(__MAIN_THREAD__).toBe(false);
    const { registerWorklet } = await import('@lynx-js/react/worklet-runtime/init');
    expect(__MAIN_THREAD__).toBe(false);

    const owner = vi.spyOn(globalThis, 'registerWorkletInternal');
    const worklet = vi.fn(() => 42);
    try {
      registerWorklet('main-thread', 'mixed-init-entry', worklet);
      expect(owner).toHaveBeenCalledWith('main-thread', 'mixed-init-entry', worklet);
      expect(globalThis.lynxWorkletImpl._workletMap['mixed-init-entry']).toBe(worklet);
    } finally {
      owner.mockRestore();
      delete globalThis.lynxWorkletImpl._workletMap['mixed-init-entry'];
    }
  });

  it('main-thread script should work', async () => {
    const cb = vi.fn();
    const Comp = () => {
      return (
        <view
          main-thread:bindtap={(e) => {
            'main thread';
            cb(e);
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };
    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: false,
    });
    expect(container).toMatchInlineSnapshot(`
      <page>
        <view>
          <text>
            Hello Main Thread Script
          </text>
        </view>
      </page>
    `);
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    expect(cb).toBeCalledTimes(1);
    expect(cb.mock.calls).toMatchInlineSnapshot(`
      [
        [
          {
            "eventName": "tap",
            "eventType": "bindEvent",
            "isTrusted": false,
            "key": "value",
          },
        ],
      ]
    `);
  });
  it('main-thread script should not throw when enable background thread', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);

    globalThis.cb = vi.fn();
    const mainThreadFn = () => {
      'main thread';
      console.log('main thread');
      globalThis.cb();
    };

    const Comp = () => {
      return (
        <view
          main-thread:bindtap={mainThreadFn}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };
    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });

    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`
      [
        [
          "rLynxChange",
          {
            "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:2","_workletType":"main-thread","_execId":"[number]"}],"id":2}]}",
            "patchOptions": {
              "isHydration": true,
              "pipelineOptions": {
                "dsl": "reactLynx",
                "needTimestamps": true,
                "pipelineID": "pipelineID",
                "pipelineOrigin": "reactLynxHydrate",
                "stage": "hydrate",
              },
              "reloadVersion": 0,
            },
          },
          [Function],
        ],
      ]
    `);
    expect(container).toMatchInlineSnapshot(`
      <page>
        <view>
          <text>
            Hello Main Thread Script
          </text>
        </view>
      </page>
    `);
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    expect(globalThis.cb).toBeCalledTimes(1);
    expect(globalThis.cb.mock.calls).toMatchInlineSnapshot(`
      [
        [],
      ]
    `);
    vi.resetAllMocks();
  });
  it('main-thread script should not update MTS function when enable background', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);

    globalThis.cb = vi.fn();
    const mainThreadFn = (e) => {
      'main thread';
      console.log('main thread');
      globalThis.cb(e);
    };

    const Comp = (props) => {
      const {
        onClick,
      } = props;

      return (
        <view
          bindtap={e => {
            if (onClick) {
              onClick(e);
            }
          }}
          main-thread:bindtap={(e) => {
            'main thread';
            if (props['main-thread:onClick']) {
              props['main-thread:onClick'](e);
            }
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };
    const { container } = render(<Comp main-thread:onClick={mainThreadFn} />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });

    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`
      [
        [
          "rLynxChange",
          {
            "data": "{"patchList":[{"snapshotPatch":[3,-2,1,{"_c":{"props":{"main-thread:onClick":{"_wkltId":"15ab:test:3"}}},"_wkltId":"15ab:test:4","_execId":"[number]"}],"id":2}]}",
            "patchOptions": {
              "isHydration": true,
              "pipelineOptions": {
                "dsl": "reactLynx",
                "needTimestamps": true,
                "pipelineID": "pipelineID",
                "pipelineOrigin": "reactLynxHydrate",
                "stage": "hydrate",
              },
              "reloadVersion": 0,
            },
          },
          [Function],
        ],
      ]
    `);
    expect(container).toMatchInlineSnapshot(`
      <page>
        <view>
          <text>
            Hello Main Thread Script
          </text>
        </view>
      </page>
    `);
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    expect(globalThis.cb).toBeCalledTimes(1);
    expect(globalThis.cb.mock.calls).toMatchInlineSnapshot(`
      [
        [
          {
            "eventName": "tap",
            "eventType": "bindEvent",
            "isTrusted": false,
            "key": "value",
          },
        ],
      ]
    `);
    vi.resetAllMocks();
  });

  it('main thread script props', () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);

    globalThis.cb = vi.fn();
    const mainThreadFn = (e) => {
      'main thread';
      console.log('main thread');
      globalThis.cb(e);
    };

    const List = (props) => {
      return (
        <view
          main-thread:bindscroll={e => {
            'main thread';
            if (props['main-thread:onScroll']) {
              props['main-thread:onScroll'](e);
            }
          }}
        >
        </view>
      );
    };

    const { container } = render(
      <List main-thread:onScroll={mainThreadFn}></List>,
      {
        enableMainThread: true,
        enableBackgroundThread: true,
      },
    );

    expect(container).toMatchInlineSnapshot(`
      <page>
        <view />
      </page>
    `);

    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`
      [
        [
          "rLynxChange",
          {
            "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_c":{"props":{"main-thread:onScroll":{"_wkltId":"15ab:test:5"}}},"_wkltId":"15ab:test:6","_execId":"[number]"}],"id":2}]}",
            "patchOptions": {
              "isHydration": true,
              "pipelineOptions": {
                "dsl": "reactLynx",
                "needTimestamps": true,
                "pipelineID": "pipelineID",
                "pipelineOrigin": "reactLynxHydrate",
                "stage": "hydrate",
              },
              "reloadVersion": 0,
            },
          },
          [Function],
        ],
      ]
    `);

    const list = container.firstChild;
    fireEvent.scroll(list, {
      info: {
        detail: {
          scrollTop: 100,
          scrollLeft: 0,
        },
      },
    });
    expect(globalThis.cb).toBeCalledTimes(1);
  });

  it('runOnMainThread works', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);
    const Comp = () => {
      return (
        <view
          bindtap={async (e) => {
            const resp = await runOnMainThread(() => {
              'main thread';
              console.log('run on main thread');
              return `Hello from main thread, __MAIN_THREAD__: ${__MAIN_THREAD__}`;
            })();
            expect(resp).toMatchInlineSnapshot(`"Hello from main thread, __MAIN_THREAD__: true"`);
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };

    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    await waitSchedule();
  });

  it('runOnBackground works', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);

    const cb = vi.fn();
    globalThis.receiveRunOnBackgroundResp = vi.fn();
    const Comp = () => {
      return (
        <view
          main-thread:bindtap={async (e) => {
            'main thread';
            const resp = await runOnBackground(() => {
              console.log('run on background');
              cb();
              return `Hello from background, __MAIN_THREAD__: ${__MAIN_THREAD__}`;
            })();
            globalThis.receiveRunOnBackgroundResp(resp);
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };
    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`
      [
        [
          "rLynxChange",
          {
            "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:8","_jsFn":{"_jsFn1":{"_jsFnId":2,"_fn":"[BackgroundFunction]"}},"_execId":"[number]"}],"id":2}]}",
            "patchOptions": {
              "isHydration": true,
              "pipelineOptions": {
                "dsl": "reactLynx",
                "needTimestamps": true,
                "pipelineID": "pipelineID",
                "pipelineOrigin": "reactLynxHydrate",
                "stage": "hydrate",
              },
              "reloadVersion": 0,
            },
          },
          [Function],
        ],
      ]
    `);
    expect(container).toMatchInlineSnapshot(`
      <page>
        <view>
          <text>
            Hello Main Thread Script
          </text>
        </view>
      </page>
    `);
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    expect(cb).toBeCalledTimes(1);
    expect(cb.mock.calls).toMatchInlineSnapshot(`
      [
        [],
      ]
    `);
    // wait for runOnBackground to finish
    await waitSchedule();
    expect(globalThis.receiveRunOnBackgroundResp).toBeCalledTimes(1);
    expect(globalThis.receiveRunOnBackgroundResp.mock.calls)
      .toMatchInlineSnapshot(`
        [
          [
            "Hello from background, __MAIN_THREAD__: false",
          ],
        ]
      `);
    vi.resetAllMocks();
  });

  it('worklet ref should work', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);
    globalThis.cb = vi.fn();
    const Comp = () => {
      const ref = useMainThreadRef(null);
      const num = useMainThreadRef(0);

      const handleTap = () => {
        'main thread';
        ref.current?.setStyleProperty('background-color', 'blue');
        num.current = 100;
        globalThis.cb(num.current);
      };

      return (
        <view
          main-thread:ref={ref}
          main-thread:bindtap={handleTap}
          style={{ width: '300px', height: '300px' }}
        >
          <text>Hello main thread ref</text>
        </view>
      );
    };

    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });

    expect(container).toMatchInlineSnapshot(`
      <page>
        <view
          has-react-ref="true"
          style="width:300px;height:300px"
        >
          <text>
            Hello main thread ref
          </text>
        </view>
      </page>
    `);
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`
      [
        [
          "rLynxChangeRefInitValue",
          {
            "data": "[[1,null],[2,0]]",
          },
        ],
        [
          "rLynxChange",
          {
            "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"15ab:test:9","_execId":"[number]"}],"id":2}]}",
            "patchOptions": {
              "isHydration": true,
              "pipelineOptions": {
                "dsl": "reactLynx",
                "needTimestamps": true,
                "pipelineID": "pipelineID",
                "pipelineOrigin": "reactLynxHydrate",
                "stage": "hydrate",
              },
              "reloadVersion": 0,
            },
          },
          [Function],
        ],
      ]
    `);
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    expect(globalThis.cb).toBeCalledTimes(1);
    expect(globalThis.cb.mock.calls).toMatchInlineSnapshot(`
      [
        [
          100,
        ],
      ]
    `);
  });

  it('nest runOnMainThread->runOnBackground works', async () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);
    const Comp = () => {
      return (
        <view
          bindtap={async (e) => {
            const resp = await runOnMainThread(() => {
              'main thread';
              console.log('run on main thread');
              runOnBackground(() => {
                console.log('run on background thread');
              })();
              return `Hello from main thread, __MAIN_THREAD__: ${__MAIN_THREAD__}`;
            })();
            expect(resp).toMatchInlineSnapshot(`"Hello from main thread, __MAIN_THREAD__: true"`);
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };

    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
  });

  it('nest runOnBackground->runOnMainThread works', async () => {
    globalThis.receiveRunOnBackgroundResp = vi.fn();
    const Comp = () => {
      return (
        <view
          main-thread:bindtap={async (e) => {
            'main thread';
            const resp = await runOnBackground(() => {
              console.log('run on background');
              runOnMainThread(() => {
                console.log('run on main thread');
              })();
              return `Hello from background, __MAIN_THREAD__: ${__MAIN_THREAD__}`;
            })();
            globalThis.receiveRunOnBackgroundResp(resp);
          }}
        >
          <text>Hello Main Thread Script</text>
        </view>
      );
    };
    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });
    fireEvent.tap(container.firstChild, {
      key: 'value',
    });
    // wait for runOnBackground to finish
    await waitSchedule();
    expect(globalThis.receiveRunOnBackgroundResp).toBeCalledTimes(1);
    expect(globalThis.receiveRunOnBackgroundResp.mock.calls)
      .toMatchInlineSnapshot(`
        [
          [
            "Hello from background, __MAIN_THREAD__: false",
          ],
        ]
      `);
    vi.resetAllMocks();
  });

  it('multiple main-thread worklets should work together when background thread is enabled', () => {
    const callLepusMethod = vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
    expect(callLepusMethod.mock.calls).toMatchInlineSnapshot(`[]`);

    globalThis.firstCb = vi.fn();
    globalThis.secondCb = vi.fn();

    const Comp = () => {
      return (
        <view>
          <view
            main-thread:bindtap={(event) => {
              'main thread';
              globalThis.firstCb(event.key);
            }}
          >
            <text>first</text>
          </view>
          <view
            main-thread:bindtap={(event) => {
              'main thread';
              globalThis.secondCb(event.key);
            }}
          >
            <text>second</text>
          </view>
        </view>
      );
    };

    const { container, getByText } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });

    expect(callLepusMethod.mock.calls).toHaveLength(1);
    expect(callLepusMethod.mock.calls[0][0]).toBe('rLynxChange');

    const patchData = JSON.parse(callLepusMethod.mock.calls[0][1].data);
    const workletIds = patchData.patchList
      .flatMap(item => Array.isArray(item.snapshotPatch) ? item.snapshotPatch : [])
      .filter(item => typeof item === 'object' && item !== null && '_wkltId' in item)
      .map(item => item._wkltId);

    expect(workletIds).toHaveLength(2);
    expect(new Set(workletIds).size).toBe(2);

    const firstView = getByText('first').parentNode;
    const secondView = getByText('second').parentNode;
    fireEvent.tap(firstView, {
      key: 'first-key',
    });
    fireEvent.tap(secondView, {
      key: 'second-key',
    });

    expect(globalThis.firstCb).toBeCalledTimes(1);
    expect(globalThis.firstCb).toBeCalledWith('first-key');
    expect(globalThis.secondCb).toBeCalledTimes(1);
    expect(globalThis.secondCb).toBeCalledWith('second-key');
    vi.resetAllMocks();
  });

  it('preserves a guarded main-thread object captured from a class field', () => {
    globalThis.classCaptureCb = vi.fn();

    class ClassCapture extends Component {
      value = this.props.value;

      onTap() {
        'main thread';
        globalThis.classCaptureCb(this.value.get());
      }

      render() {
        return <view main-thread:bindtap={this.onTap} />;
      }
    }

    const Comp = () => {
      const value = useMainThreadObject(guardedClassCaptureType, 42);
      return <ClassCapture value={value} />;
    };

    const { container } = render(<Comp />, {
      enableMainThread: true,
      enableBackgroundThread: true,
    });
    fireEvent.tap(container.firstChild);

    expect(globalThis.classCaptureCb).toHaveBeenCalledWith(42);
  });
});
