import { describe, expect, it } from "vitest";

import { createEditorStore } from "../../src/app/editor-store";
import { createCreateBoxBrushCommand } from "../../src/commands/create-box-brush-command";
import { createCreateSceneCommand } from "../../src/commands/create-scene-command";
import { createSetActiveSceneCommand } from "../../src/commands/set-active-scene-command";
import { createSetSceneLoadingScreenCommand } from "../../src/commands/set-scene-loading-screen-command";
import { createSetSceneNameCommand } from "../../src/commands/set-scene-name-command";
import { createTransformSession } from "../../src/core/transform-session";
import { createBoxBrush } from "../../src/document/brushes";
import {
  createEmptyProjectScene,
  createEmptySceneDocument
} from "../../src/document/scene-document";
import type { KeyValueStorage } from "../../src/serialization/local-draft-storage";

class MemoryStorage implements KeyValueStorage {
  private readonly values = new Map<string, string>();

  getItem(key: string): string | null {
    return this.values.get(key) ?? null;
  }

  setItem(key: string, value: string): void {
    this.values.set(key, value);
  }

  removeItem(key: string): void {
    this.values.delete(key);
  }
}

class ThrowingStorage implements KeyValueStorage {
  getItem(): string | null {
    throw new Error("blocked read");
  }

  setItem(): void {
    throw new Error("blocked write");
  }

  removeItem(): void {}
}

describe("EditorStore", () => {
  it("returns a stable snapshot between store updates", () => {
    const store = createEditorStore();

    const initialSnapshot = store.getState();
    const repeatedSnapshot = store.getState();

    expect(repeatedSnapshot).toBe(initialSnapshot);

    store.executeCommand(createSetSceneNameCommand("Snapshot Scene"));

    const updatedSnapshot = store.getState();
    expect(updatedSnapshot).not.toBe(initialSnapshot);
    expect(updatedSnapshot.document.name).toBe("Snapshot Scene");
  });

  it("applies command history with undo and redo", () => {
    const store = createEditorStore();

    store.executeCommand(createSetSceneNameCommand("Foundation Room"));

    expect(store.getState().document.name).toBe("Foundation Room");
    expect(store.getState().canUndo).toBe(true);

    expect(store.undo()).toBe(true);
    expect(store.getState().document.name).toBe("Untitled Scene");
    expect(store.getState().canRedo).toBe(true);

    expect(store.redo()).toBe(true);
    expect(store.getState().document.name).toBe("Foundation Room");
  });

  it("creates scenes, switches the active scene, and keeps inactive scene content in the project document", () => {
    const store = createEditorStore();

    store.executeCommand(createSetSceneNameCommand("Entry"));
    const firstSceneId = store.getState().activeSceneId;

    store.executeCommand(createCreateSceneCommand());
    const secondSceneId = store.getState().activeSceneId;

    expect(secondSceneId).not.toBe(firstSceneId);
    expect(Object.keys(store.getState().projectDocument.scenes)).toHaveLength(
      2
    );
    expect(store.getState().document.name).toBe("Scene 2");

    store.executeCommand(createCreateBoxBrushCommand());

    expect(
      Object.keys(store.getState().projectDocument.scenes[firstSceneId].brushes)
    ).toHaveLength(0);
    expect(
      Object.keys(
        store.getState().projectDocument.scenes[secondSceneId].brushes
      )
    ).toHaveLength(1);

    store.executeCommand(createSetActiveSceneCommand(firstSceneId));

    expect(store.getState().activeSceneId).toBe(firstSceneId);
    expect(store.getState().document.name).toBe("Entry");
    expect(Object.keys(store.getState().document.brushes)).toHaveLength(0);
    expect(
      Object.keys(
        store.getState().projectDocument.scenes[secondSceneId].brushes
      )
    ).toHaveLength(1);
  });

  it("restores scene-scoped editor preferences when switching scenes", () => {
    const store = createEditorStore();
    const firstSceneId = store.getState().activeSceneId;

    store.setWhiteboxSelectionMode("face");
    store.setWhiteboxSnapEnabled(false);
    store.setWhiteboxSnapStep(0.5);
    store.setViewportGridVisible(false);
    store.setViewportLayoutMode("quad");
    store.setActiveViewportPanel("bottomRight");
    store.setViewportPanelViewMode("topLeft", "front");
    store.setViewportPanelDisplayMode("topLeft", "wireframe");

    store.executeCommand(createCreateSceneCommand());
    const secondSceneId = store.getState().activeSceneId;

    expect(secondSceneId).not.toBe(firstSceneId);
    expect(store.getState().whiteboxSelectionMode).toBe("object");
    expect(store.getState().whiteboxSnapEnabled).toBe(true);
    expect(store.getState().whiteboxSnapStep).toBe(1);
    expect(store.getState().viewportGridVisible).toBe(true);
    expect(store.getState().viewportLayoutMode).toBe("single");
    expect(store.getState().activeViewportPanelId).toBe("topLeft");
    expect(store.getState().viewportPanels.topLeft.viewMode).toBe(
      "perspective"
    );
    expect(store.getState().viewportPanels.topLeft.displayMode).toBe(
      "normal"
    );

    store.setWhiteboxSelectionMode("vertex");
    store.setWhiteboxSnapEnabled(true);
    store.setWhiteboxSnapStep(2);
    store.setViewportGridVisible(true);
    store.setViewportLayoutMode("quad");
    store.setActiveViewportPanel("topRight");
    store.setViewportPanelViewMode("topLeft", "side");
    store.setViewportPanelDisplayMode("topLeft", "authoring");

    store.executeCommand(createSetActiveSceneCommand(firstSceneId));

    expect(store.getState().whiteboxSelectionMode).toBe("face");
    expect(store.getState().whiteboxSnapEnabled).toBe(false);
    expect(store.getState().whiteboxSnapStep).toBe(0.5);
    expect(store.getState().viewportGridVisible).toBe(false);
    expect(store.getState().viewportLayoutMode).toBe("quad");
    expect(store.getState().activeViewportPanelId).toBe("bottomRight");
    expect(store.getState().viewportPanels.topLeft.viewMode).toBe("front");
    expect(store.getState().viewportPanels.topLeft.displayMode).toBe(
      "wireframe"
    );

    store.executeCommand(createSetActiveSceneCommand(secondSceneId));

    expect(store.getState().whiteboxSelectionMode).toBe("vertex");
    expect(store.getState().whiteboxSnapEnabled).toBe(true);
    expect(store.getState().whiteboxSnapStep).toBe(2);
    expect(store.getState().viewportGridVisible).toBe(true);
    expect(store.getState().viewportLayoutMode).toBe("quad");
    expect(store.getState().activeViewportPanelId).toBe("topRight");
    expect(store.getState().viewportPanels.topLeft.viewMode).toBe("side");
    expect(store.getState().viewportPanels.topLeft.displayMode).toBe(
      "authoring"
    );
  });

  it("updates scene loading overlay settings through commands and supports undo", () => {
    const store = createEditorStore();
    const sceneId = store.getState().activeSceneId;

    store.executeCommand(
      createSetSceneLoadingScreenCommand({
        sceneId,
        label: "Update scene loading overlay",
        loadingScreen: {
          colorHex: "#2b3141",
          headline: "Entering the room",
          description: "Preparing authored triggers."
        }
      })
    );

    expect(
      store.getState().projectDocument.scenes[sceneId]?.loadingScreen
    ).toEqual({
      colorHex: "#2b3141",
      headline: "Entering the room",
      description: "Preparing authored triggers."
    });

    expect(store.undo()).toBe(true);
    expect(
      store.getState().projectDocument.scenes[sceneId]?.loadingScreen
    ).toEqual({
      colorHex: "#0d1117",
      headline: null,
      description: null
    });
  });

  it("keeps scene-scoped command history targeting the authored scene after the active scene changes", () => {
    const store = createEditorStore();

    store.executeCommand(createCreateBoxBrushCommand());
    const authoredSceneId = store.getState().activeSceneId;
    const otherSceneId = "scene-other";

    store.replaceDocument(
      {
        ...store.getState().projectDocument,
        activeSceneId: otherSceneId,
        scenes: {
          ...store.getState().projectDocument.scenes,
          [otherSceneId]: createEmptyProjectScene({
            id: otherSceneId,
            name: "Other"
          })
        }
      },
      false
    );

    expect(store.getState().activeSceneId).toBe(otherSceneId);
    expect(Object.keys(store.getState().document.brushes)).toHaveLength(0);

    expect(store.undo()).toBe(true);
    expect(store.getState().activeSceneId).toBe(otherSceneId);
    expect(
      Object.keys(
        store.getState().projectDocument.scenes[authoredSceneId].brushes
      )
    ).toHaveLength(0);
  });

  it("saves and loads a local draft document", () => {
    const storage = new MemoryStorage();
    const writerStore = createEditorStore({ storage });

    writerStore.executeCommand(createSetSceneNameCommand("Draft Scene"));
    writerStore.setViewportLayoutMode("quad");
    writerStore.setActiveViewportPanel("bottomRight");
    writerStore.setViewportPanelViewMode("topLeft", "top");
    writerStore.setViewportPanelDisplayMode("topLeft", "wireframe");
    writerStore.setViewportPanelCameraState("topLeft", {
      target: {
        x: 6,
        y: 2,
        z: -4
      },
      perspectiveOrbit: {
        radius: 18,
        theta: 0.9,
        phi: 1.1
      },
      orthographicZoom: 2.25
    });
    expect(writerStore.saveDraft()).toEqual({
      status: "saved",
      message: "Autosave updated."
    });

    const readerStore = createEditorStore({
      initialDocument: createEmptySceneDocument({ name: "Fresh Scene" }),
      storage
    });

    expect(readerStore.loadDraft()).toMatchObject({
      status: "loaded",
      message: "Recovered latest autosave."
    });
    expect(readerStore.getState().document.name).toBe("Draft Scene");
    expect(readerStore.getState().viewportLayoutMode).toBe("quad");
    expect(readerStore.getState().activeViewportPanelId).toBe("bottomRight");
    expect(readerStore.getState().viewportPanels.topLeft).toMatchObject({
      viewMode: "top",
      displayMode: "wireframe",
      cameraState: {
        target: {
          x: 6,
          y: 2,
          z: -4
        },
        perspectiveOrbit: {
          radius: 18,
          theta: 0.9,
          phi: 1.1
        },
        orthographicZoom: 2.25
      }
    });
  });

  it("fails gracefully when storage access throws", () => {
    const store = createEditorStore({ storage: new ThrowingStorage() });

    expect(store.saveDraft()).toMatchObject({
      status: "error",
      message: expect.stringContaining("blocked write")
    });

    expect(store.loadDraft()).toMatchObject({
      status: "error",
      message: expect.stringContaining("blocked read")
    });
  });

  it("restores the previous editor tool when leaving play mode", () => {
    const store = createEditorStore();

    store.setToolMode("create");
    store.enterPlayMode();

    expect(store.getState().toolMode).toBe("play");

    store.exitPlayMode();

    expect(store.getState().toolMode).toBe("create");
  });

  it("tracks viewport layout and per-panel state independently from the document", () => {
    const store = createEditorStore();

    expect(store.getState().whiteboxSelectionMode).toBe("object");
    expect(store.getState().viewportLayoutMode).toBe("single");
    expect(store.getState().activeViewportPanelId).toBe("topLeft");
    expect(store.getState().viewportPanels.topLeft.viewMode).toBe(
      "perspective"
    );
    expect(store.getState().viewportPanels.topRight.viewMode).toBe("top");
    expect(store.getState().viewportPanels.topRight.displayMode).toBe(
      "authoring"
    );
    expect(store.getState().viewportQuadSplit).toEqual({
      x: 0.5,
      y: 0.5
    });

    store.setViewportLayoutMode("quad");
    store.setActiveViewportPanel("bottomRight");
    store.setViewportPanelViewMode("bottomRight", "front");
    store.setViewportPanelDisplayMode("bottomRight", "normal");
    store.setViewportQuadSplit({
      x: 0.38,
      y: 0.62
    });

    expect(store.getState().viewportLayoutMode).toBe("quad");
    expect(store.getState().activeViewportPanelId).toBe("bottomRight");
    expect(store.getState().viewportPanels.bottomRight.viewMode).toBe("front");
    expect(store.getState().viewportPanels.bottomRight.displayMode).toBe(
      "normal"
    );
    expect(store.getState().viewportQuadSplit).toEqual({
      x: 0.38,
      y: 0.62
    });
  });

  it("tracks whitebox component selection mode independently from document state", () => {
    const store = createEditorStore();

    store.setWhiteboxSelectionMode("face");
    expect(store.getState().whiteboxSelectionMode).toBe("face");

    store.setWhiteboxSelectionMode("edge");
    expect(store.getState().whiteboxSelectionMode).toBe("edge");

    store.setWhiteboxSelectionMode("vertex");
    expect(store.getState().whiteboxSelectionMode).toBe("vertex");

    store.setWhiteboxSelectionMode("object");
    expect(store.getState().whiteboxSelectionMode).toBe("object");
  });

  it("normalizes selected whitebox components back to the owning solid when switching to a different component mode", () => {
    const store = createEditorStore();

    store.executeCommand(createCreateBoxBrushCommand());
    const createdBrush = Object.values(store.getState().document.brushes)[0];

    store.setWhiteboxSelectionMode("face");
    store.setSelection({
      kind: "brushFace",
      brushId: createdBrush.id,
      faceId: "posY"
    });

    expect(store.getState().selection).toEqual({
      kind: "brushFace",
      brushId: createdBrush.id,
      faceId: "posY"
    });

    store.setWhiteboxSelectionMode("edge");
    expect(store.getState().selection).toEqual({
      kind: "brushes",
      ids: [createdBrush.id]
    });

    store.setSelection({
      kind: "brushEdge",
      brushId: createdBrush.id,
      edgeId: "edgeX_posY_negZ"
    });
    store.setWhiteboxSelectionMode("vertex");
    expect(store.getState().selection).toEqual({
      kind: "brushes",
      ids: [createdBrush.id]
    });

    store.setSelection({
      kind: "brushVertex",
      brushId: createdBrush.id,
      vertexId: "posX_posY_negZ"
    });
    store.setWhiteboxSelectionMode("object");
    expect(store.getState().selection).toEqual({
      kind: "brushes",
      ids: [createdBrush.id]
    });
  });

  it("shares transient creation preview state across viewport panels", () => {
    const store = createEditorStore();

    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "none"
    });

    store.setViewportToolPreview({
      kind: "create",
      sourcePanelId: "topLeft",
      target: {
        kind: "box-brush"
      },
      center: {
        x: 4,
        y: 0,
        z: 8
      }
    });

    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "create",
      sourcePanelId: "topLeft",
      target: {
        kind: "box-brush"
      },
      center: {
        x: 4,
        y: 0,
        z: 8
      }
    });

    store.setViewportToolPreview({
      kind: "create",
      sourcePanelId: "bottomRight",
      target: {
        kind: "entity",
        entityKind: "pointLight",
        audioAssetId: null,
        modelAssetId: null
      },
      center: {
        x: 2,
        y: 1,
        z: -3
      }
    });

    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "create",
      sourcePanelId: "bottomRight",
      target: {
        kind: "entity",
        entityKind: "pointLight",
        audioAssetId: null,
        modelAssetId: null
      },
      center: {
        x: 2,
        y: 1,
        z: -3
      }
    });

    store.clearViewportToolPreview("topRight");
    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "create",
      sourcePanelId: "bottomRight",
      target: {
        kind: "entity",
        entityKind: "pointLight",
        audioAssetId: null,
        modelAssetId: null
      },
      center: {
        x: 2,
        y: 1,
        z: -3
      }
    });

    store.clearViewportToolPreview("bottomRight");
    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "none"
    });
  });

  it("tracks a shared transient transform session and clears it when selection changes", () => {
    const store = createEditorStore();

    store.setTransformSession(
      createTransformSession({
        source: "keyboard",
        sourcePanelId: "bottomRight",
        operation: "translate",
        target: {
          kind: "brush",
          brushId: "brush-main",
          brushKind: "box",
          initialCenter: {
            x: 0,
            y: 1,
            z: 0
          },
          initialRotationDegrees: {
            x: 0,
            y: 0,
            z: 0
          },
          initialSize: {
            x: 2,
            y: 2,
            z: 2
          },
          initialGeometry: createBoxBrush({
            size: {
              x: 2,
              y: 2,
              z: 2
            }
          }).geometry
        }
      })
    );

    expect(
      store.getState().viewportTransientState.transformSession
    ).toMatchObject({
      kind: "active",
      source: "keyboard",
      sourcePanelId: "bottomRight",
      operation: "translate",
      target: {
        kind: "brush",
        brushId: "brush-main"
      },
      preview: {
        kind: "brush",
        center: {
          x: 0,
          y: 1,
          z: 0
        },
        rotationDegrees: {
          x: 0,
          y: 0,
          z: 0
        },
        size: {
          x: 2,
          y: 2,
          z: 2
        }
      }
    });

    store.setSelection({
      kind: "brushes",
      ids: ["brush-main"]
    });

    expect(store.getState().viewportTransientState.transformSession).toEqual({
      kind: "none"
    });
  });

  it("clears transient viewport preview when leaving create mode", () => {
    const store = createEditorStore();

    store.setToolMode("create");
    store.setViewportToolPreview({
      kind: "create",
      sourcePanelId: "bottomRight",
      target: {
        kind: "model-instance",
        assetId: "asset-1"
      },
      center: null
    });
    store.setToolMode("select");

    expect(store.getState().viewportTransientState.toolPreview).toEqual({
      kind: "none"
    });
  });

  it("tracks the active id for same-kind multi-selection and falls back to the last remaining id", () => {
    const brushA = createBoxBrush({
      id: "brush-active-a"
    });
    const brushB = createBoxBrush({
      id: "brush-active-b"
    });
    const brushC = createBoxBrush({
      id: "brush-active-c"
    });
    const store = createEditorStore({
      initialDocument: {
        ...createEmptySceneDocument(),
        brushes: {
          [brushA.id]: brushA,
          [brushB.id]: brushB,
          [brushC.id]: brushC
        }
      }
    });

    store.setSelection({
      kind: "brushes",
      ids: [brushA.id, brushB.id, brushC.id]
    });

    expect(store.getState().activeSelectionId).toBe(brushC.id);

    store.setSelection({
      kind: "brushes",
      ids: [brushA.id, brushB.id]
    });

    expect(store.getState().activeSelectionId).toBe(brushB.id);
  });

  it("does not add selection-only changes to undo or redo history", () => {
    const store = createEditorStore();

    store.executeCommand(createSetSceneNameCommand("Selection History Fixture"));

    store.setSelection({
      kind: "entities",
      ids: ["entity-selection-only"]
    });
    store.setSelection({
      kind: "none"
    });

    expect(store.undo()).toBe(true);
    expect(store.getState().document.name).toBe("Untitled Scene");
    expect(store.getState().selection).toEqual({
      kind: "none"
    });

    expect(store.redo()).toBe(true);
    expect(store.getState().document.name).toBe("Selection History Fixture");
    expect(store.getState().selection).toEqual({
      kind: "none"
    });
  });
});
