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

import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftState } from "../../state/types";
import { CanvasShell } from "../canvas/canvas-shell";
import { ControlsPanel } from "../controls-panel/controls-panel";
import { LayersPanel } from "../layers/layers-panel";
import { ToolcraftPanelHost } from "../panel-host/panel-host";
import { TimelinePanel } from "../timeline/timeline-panel";
import { ToolbarPanel } from "./toolbar-panel";
import { ToolcraftRoot } from "./toolcraft-root";
import {
  useToolcraftCommittedSelector,
  useToolcraftDependencySelector,
} from "./toolcraft-selectors";
import { useToolcraftStore } from "./toolcraft-store-context";

const isolationSchema = defineToolcraft({
  canvas: { enabled: true },
  panels: {
    controls: {
      sections: [
        {
          controls: {
            opacity: {
              defaultValue: 75,
              target: "shape.opacity",
              type: "slider",
            },
          },
        },
      ],
      title: "Controls",
    },
    layers: true,
    timeline: { mode: "playback" },
  },
  persistence: {
    include: ["canvas", "panels", "timeline"],
    key: "toolcraft:render-isolation:state:v1",
    storage: "localStorage",
    version: 1,
  },
  toolbar: {
    history: true,
    radar: true,
    theme: true,
    zoom: true,
  },
});

type CommitName =
  | "canvas"
  | "controls"
  | "layers"
  | "panel"
  | "playback"
  | "toolbar";

function createCommitCounts(): Record<CommitName, number> {
  return {
    canvas: 0,
    controls: 0,
    layers: 0,
    panel: 0,
    playback: 0,
    toolbar: 0,
  };
}

function CommitCounter({
  children,
  counts,
  name,
}: {
  children: React.ReactNode;
  counts: Record<CommitName, number>;
  name: CommitName;
}): React.JSX.Element {
  return (
    <React.Profiler
      id={name}
      onRender={(_id, phase) => {
        if (phase === "update") {
          counts[name] += 1;
        }
      }}
    >
      {children}
    </React.Profiler>
  );
}

function CaptureStore({
  capture,
}: {
  capture: (store: ToolcraftExternalStore) => void;
}): null {
  capture(useToolcraftStore());
  return null;
}

function ControlsSelectorProbe({
  execute,
}: {
  execute: ReturnType<typeof vi.fn>;
}): null {
  const selector = React.useCallback(
    (state: ToolcraftState) => {
      execute();
      return state;
    },
    [execute],
  );
  useToolcraftCommittedSelector(selector);
  return null;
}

function PlaybackSelectorProbe({
  execute,
}: {
  execute: ReturnType<typeof vi.fn>;
}): null {
  useToolcraftDependencySelector(
    (state) => {
      execute();
      return state.timeline.currentTimeSeconds;
    },
    Object.is,
    [{ kind: "playback" }],
  );
  return null;
}

function ViewportSelectorProbe({
  execute,
}: {
  execute: ReturnType<typeof vi.fn>;
}): null {
  useToolcraftDependencySelector(
    (state) => {
      execute();
      return `${state.canvas.offset.x}:${state.canvas.offset.y}:${state.canvas.zoom}`;
    },
    Object.is,
    [{ kind: "viewport.offset" }, { kind: "viewport.zoom" }],
  );
  return null;
}

function IsolationHarness({
  capture,
  counts,
  selectorExecutions,
}: {
  capture: (store: ToolcraftExternalStore) => void;
  counts: Record<CommitName, number>;
  selectorExecutions: {
    controls: ReturnType<typeof vi.fn>;
    playback: ReturnType<typeof vi.fn>;
    viewport: ReturnType<typeof vi.fn>;
  };
}): React.JSX.Element {
  return (
    <ToolcraftRoot
      initialState={{ timeline: { isPlaying: false } }}
      schema={isolationSchema}
    >
      <CaptureStore capture={capture} />
      <ControlsSelectorProbe execute={selectorExecutions.controls} />
      <PlaybackSelectorProbe execute={selectorExecutions.playback} />
      <ViewportSelectorProbe execute={selectorExecutions.viewport} />
      <CommitCounter counts={counts} name="controls">
        <ControlsPanel framed={false} />
      </CommitCounter>
      <CommitCounter counts={counts} name="layers">
        <LayersPanel framed={false} />
      </CommitCounter>
      <CommitCounter counts={counts} name="toolbar">
        <ToolbarPanel framed={false} />
      </CommitCounter>
      <CommitCounter counts={counts} name="panel">
        <ToolcraftPanelHost panelType="toolbar">Panel</ToolcraftPanelHost>
      </CommitCounter>
      <CommitCounter counts={counts} name="playback">
        <TimelinePanel framed={false} />
      </CommitCounter>
      <CommitCounter counts={counts} name="canvas">
        <div style={{ height: 480, position: "relative", width: 640 }}>
          <CanvasShell />
        </div>
      </CommitCounter>
    </ToolcraftRoot>
  );
}

beforeAll(() => {
  Object.defineProperty(window, "matchMedia", {
    configurable: true,
    value: (query: string) => ({
      addEventListener: () => undefined,
      addListener: () => undefined,
      dispatchEvent: () => false,
      matches: query === "(prefers-reduced-motion: reduce)",
      media: query,
      onchange: null,
      removeEventListener: () => undefined,
      removeListener: () => undefined,
    }),
  });
});

afterEach(() => {
  vi.useRealTimers();
  cleanup();
  window.localStorage.clear();
  vi.restoreAllMocks();
});

describe("Toolcraft hot render isolation", () => {
  it("keeps controls cold when committing a transient playback update", () => {
    const counts = createCommitCounts();
    const selectorExecutions = {
      controls: vi.fn(),
      playback: vi.fn(),
      viewport: vi.fn(),
    };
    let store: ToolcraftExternalStore | undefined;

    render(
      <IsolationHarness
        capture={(value) => {
          store = value;
        }}
        counts={counts}
        selectorExecutions={selectorExecutions}
      />,
    );
    const initialExecutions = {
      controls: selectorExecutions.controls.mock.calls.length,
      playback: selectorExecutions.playback.mock.calls.length,
      viewport: selectorExecutions.viewport.mock.calls.length,
    };

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

    expect(counts).toEqual({
      canvas: 0,
      controls: 0,
      layers: 0,
      panel: 0,
      playback: 1,
      toolbar: 0,
    });
    expect(selectorExecutions.playback.mock.calls.length).toBeGreaterThan(
      initialExecutions.playback,
    );
    expect(selectorExecutions.controls).toHaveBeenCalledTimes(
      initialExecutions.controls,
    );
    expect(selectorExecutions.viewport).toHaveBeenCalledTimes(
      initialExecutions.viewport,
    );

    act(() => {
      store?.commitTransient("playback");
    });

    expect(counts).toEqual({
      canvas: 0,
      controls: 0,
      layers: 0,
      panel: 0,
      playback: 2,
      toolbar: 0,
    });
    expect(selectorExecutions.controls.mock.calls.length).toBeGreaterThan(
      initialExecutions.controls,
    );

    const controlsExecutionsAfterTransientCommit =
      selectorExecutions.controls.mock.calls.length;

    act(() => {
      store?.dispatch({
        target: "shape.opacity",
        type: "controls.setValue",
        value: 80,
      });
    });

    expect(counts.controls).toBeGreaterThan(0);
    expect(selectorExecutions.controls.mock.calls.length).toBeGreaterThan(
      controlsExecutionsAfterTransientCommit,
    );
  });

  it("keeps controls cold when committing transient viewport moves", () => {
    const counts = createCommitCounts();
    const selectorExecutions = {
      controls: vi.fn(),
      playback: vi.fn(),
      viewport: vi.fn(),
    };
    let store: ToolcraftExternalStore | undefined;

    render(
      <IsolationHarness
        capture={(value) => {
          store = value;
        }}
        counts={counts}
        selectorExecutions={selectorExecutions}
      />,
    );
    const initialExecutions = {
      controls: selectorExecutions.controls.mock.calls.length,
      playback: selectorExecutions.playback.mock.calls.length,
      viewport: selectorExecutions.viewport.mock.calls.length,
    };

    act(() => {
      for (let index = 1; index <= 5; index += 1) {
        store?.dispatchTransient({
          offset: { x: index * 3, y: index * -2 },
          type: "canvas.setViewport",
          zoom: 100 + index,
        });
      }
    });

    expect(counts).toEqual({
      canvas: 1,
      controls: 0,
      layers: 0,
      panel: 0,
      playback: 0,
      toolbar: 0,
    });
    expect(selectorExecutions.viewport.mock.calls.length).toBeGreaterThan(
      initialExecutions.viewport,
    );
    expect(selectorExecutions.controls).toHaveBeenCalledTimes(
      initialExecutions.controls,
    );
    expect(selectorExecutions.playback).toHaveBeenCalledTimes(
      initialExecutions.playback,
    );

    act(() => {
      store?.commitTransient("viewport");
    });

    expect(counts).toEqual({
      canvas: 1,
      controls: 0,
      layers: 0,
      panel: 0,
      playback: 0,
      toolbar: 1,
    });
  });

  it("does not schedule persistence until one transient commit boundary", () => {
    vi.useFakeTimers();
    const setItem = vi.spyOn(Storage.prototype, "setItem");
    let store: ToolcraftExternalStore | undefined;

    render(
      <ToolcraftRoot schema={isolationSchema}>
        <CaptureStore
          capture={(value) => {
            store = value;
          }}
        />
      </ToolcraftRoot>,
    );
    act(() => {
      vi.advanceTimersByTime(120);
    });
    setItem.mockClear();

    act(() => {
      store?.dispatchTransient({
        currentTimeSeconds: 1,
        type: "timeline.setCurrentTime",
      });
      for (let index = 1; index <= 5; index += 1) {
        store?.dispatchTransient({
          offset: { x: index, y: -index },
          type: "canvas.setOffset",
        });
      }
      vi.advanceTimersByTime(240);
    });

    expect(setItem).not.toHaveBeenCalled();

    act(() => {
      store?.commitTransient("viewport");
      vi.advanceTimersByTime(119);
    });
    expect(setItem).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(1);
    });
    expect(setItem).toHaveBeenCalledTimes(1);
  });
});
