import { act, cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { defineToolcraft } from "../../schema/define-toolcraft";
import { createToolcraftState } from "../../state/create-template-state";
import { createState } from "../../state/reducer-test-utils";
import { createToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftCommand } from "../../state/types";
import { useToolcraftStoreSelector } from "./toolcraft-selectors";
import { ToolcraftStoreContext } from "./toolcraft-store-context";

const valueDependencies = [{ kind: "value", target: "value.first" }] as const;
const otherValueDependencies = [
  { kind: "value", target: "value.second" },
] as const;
const playbackDependencies = [{ kind: "playback" }] as const;
const viewportDependencies = [{ kind: "viewport.zoom" }] as const;
const firstKeyframeDependencies = [
  { kind: "keyframeGroup", target: "value.first" },
] as const;
const secondKeyframeDependencies = [
  { kind: "keyframeGroup", target: "value.second" },
] as const;

const keyframeEditCommands: Array<{
  command: ToolcraftCommand;
  label: string;
}> = [
  {
    command: {
      keyframeId: "value.first::1",
      timeSeconds: 2,
      type: "timeline.moveKeyframe",
    },
    label: "move",
  },
  {
    command: {
      keyframeId: "value.first::1",
      type: "timeline.deleteKeyframe",
    },
    label: "delete",
  },
  {
    command: {
      easing: { type: "step" },
      keyframeId: "value.first::1",
      type: "timeline.changeKeyframeEasing",
    },
    label: "easing change",
  },
];

function createSelectorLaneStore() {
  const schema = defineToolcraft({
    canvas: { enabled: true },
    panels: {
      controls: {
        sections: [
          {
            controls: {
              first: {
                defaultValue: 1,
                target: "value.first",
                type: "slider",
              },
              second: {
                defaultValue: 2,
                target: "value.second",
                type: "slider",
              },
            },
          },
        ],
        title: "Controls",
      },
    },
  });

  return createToolcraftExternalStore(createToolcraftState(schema));
}

function createKeyframeLaneStore() {
  return createToolcraftExternalStore(
    createState({
      timeline: {
        keyframeGroups: [
          {
            controlId: "value.first",
            keyframes: [
              {
                controlId: "value.first",
                controlLabel: "First",
                id: "value.first::1",
                timeSeconds: 1,
                value: 10,
                valueLabel: "10",
              },
            ],
            label: "First",
          },
          {
            controlId: "value.second",
            keyframes: [
              {
                controlId: "value.second",
                controlLabel: "Second",
                id: "value.second::1",
                timeSeconds: 1,
                value: 20,
                valueLabel: "20",
              },
            ],
            label: "Second",
          },
        ],
      },
    }),
  );
}

function LaneProvider({
  children,
  store,
}: {
  children: React.ReactNode;
  store: ToolcraftExternalStore;
}) {
  return (
    <ToolcraftStoreContext.Provider value={store}>
      {children}
    </ToolcraftStoreContext.Provider>
  );
}

afterEach(cleanup);

describe("Toolcraft selector execution lanes", () => {
  it("does not execute selectors outside their changed dependency fields", () => {
    const store = createSelectorLaneStore();
    const executions = {
      playback: vi.fn(),
      otherValue: vi.fn(),
      value: vi.fn(),
      viewport: vi.fn(),
    };

    function ValueProbe() {
      useToolcraftStoreSelector(
        (state) => {
          executions.value();
          return state.values["value.first"];
        },
        Object.is,
        valueDependencies,
      );
      return null;
    }

    function PlaybackProbe() {
      useToolcraftStoreSelector(
        (state) => {
          executions.playback();
          return state.timeline.currentTimeSeconds;
        },
        Object.is,
        playbackDependencies,
      );
      return null;
    }

    function OtherValueProbe() {
      useToolcraftStoreSelector(
        (state) => {
          executions.otherValue();
          return state.values["value.second"];
        },
        Object.is,
        otherValueDependencies,
      );
      return null;
    }

    function ViewportProbe() {
      useToolcraftStoreSelector(
        (state) => {
          executions.viewport();
          return state.canvas.zoom;
        },
        Object.is,
        viewportDependencies,
      );
      return null;
    }

    render(
      <LaneProvider store={store}>
        <ValueProbe />
        <OtherValueProbe />
        <PlaybackProbe />
        <ViewportProbe />
      </LaneProvider>,
    );

    const initial = {
      playback: executions.playback.mock.calls.length,
      otherValue: executions.otherValue.mock.calls.length,
      value: executions.value.mock.calls.length,
      viewport: executions.viewport.mock.calls.length,
    };

    act(() => {
      store.dispatchTransient({
        currentTimeSeconds: 1,
        type: "timeline.setCurrentTime",
      });
    });

    expect(executions.playback.mock.calls.length).toBeGreaterThan(
      initial.playback,
    );
    expect(executions.value).toHaveBeenCalledTimes(initial.value);
    expect(executions.otherValue).toHaveBeenCalledTimes(initial.otherValue);
    expect(executions.viewport).toHaveBeenCalledTimes(initial.viewport);

    const afterPlayback = {
      playback: executions.playback.mock.calls.length,
      otherValue: executions.otherValue.mock.calls.length,
      value: executions.value.mock.calls.length,
      viewport: executions.viewport.mock.calls.length,
    };

    act(() => {
      store.dispatchTransient({
        offset: { x: 0, y: 0 },
        type: "canvas.setViewport",
        zoom: 150,
      });
    });

    expect(executions.viewport.mock.calls.length).toBeGreaterThan(
      afterPlayback.viewport,
    );
    expect(executions.value).toHaveBeenCalledTimes(afterPlayback.value);
    expect(executions.otherValue).toHaveBeenCalledTimes(
      afterPlayback.otherValue,
    );
    expect(executions.playback).toHaveBeenCalledTimes(
      afterPlayback.playback,
    );

    const afterViewport = {
      playback: executions.playback.mock.calls.length,
      otherValue: executions.otherValue.mock.calls.length,
      value: executions.value.mock.calls.length,
      viewport: executions.viewport.mock.calls.length,
    };

    act(() => {
      store.dispatch({
        target: "value.first",
        type: "controls.setValue",
        value: 10,
      });
    });

    expect(executions.value.mock.calls.length).toBeGreaterThan(
      afterViewport.value,
    );
    expect(executions.otherValue).toHaveBeenCalledTimes(
      afterViewport.otherValue,
    );
    expect(executions.playback).toHaveBeenCalledTimes(
      afterViewport.playback,
    );
    expect(executions.viewport).toHaveBeenCalledTimes(
      afterViewport.viewport,
    );
  });

  it.each(keyframeEditCommands)(
    "does not execute target B selector for target A $label",
    ({ command }) => {
      const store = createKeyframeLaneStore();
      const executions = { first: vi.fn(), second: vi.fn() };

      function FirstKeyframeProbe() {
        useToolcraftStoreSelector(
          (state) => {
            executions.first();
            return state.timeline.keyframeGroups.find(
              (group) => group.controlId === "value.first",
            );
          },
          Object.is,
          firstKeyframeDependencies,
        );
        return null;
      }

      function SecondKeyframeProbe() {
        useToolcraftStoreSelector(
          (state) => {
            executions.second();
            return state.timeline.keyframeGroups.find(
              (group) => group.controlId === "value.second",
            );
          },
          Object.is,
          secondKeyframeDependencies,
        );
        return null;
      }

      render(
        <LaneProvider store={store}>
          <FirstKeyframeProbe />
          <SecondKeyframeProbe />
        </LaneProvider>,
      );

      const initialFirstExecutions = executions.first.mock.calls.length;
      const initialSecondExecutions = executions.second.mock.calls.length;

      act(() => {
        store.dispatch(command);
      });

      expect(executions.first.mock.calls.length).toBeGreaterThan(
        initialFirstExecutions,
      );
      expect(executions.second).toHaveBeenCalledTimes(
        initialSecondExecutions,
      );
    },
  );
});
