import { describe, it, expect, beforeEach, vi } from "vitest";
import { useProjectStore } from "./project-store";
import { useEngineStore } from "./engine-store";
import type { Project, Clip, MediaItem, Transition } from "@openreel/core";

const {
  mockEffectsBridge,
  mockEffectsBridgeState,
  mockTransitionBridge,
  mockTransitionBridgeState,
} = vi.hoisted(() => {
  const clipEffects = new Map<string, Array<{
    id: string;
    type: string;
    enabled: boolean;
    params: Record<string, unknown>;
    order: number;
  }>>();

  const getDefaultParams = (effectType: string): Record<string, unknown> => {
    switch (effectType) {
      case "brightness":
        return { value: 0 };
      case "contrast":
        return { value: 1 };
      case "saturation":
        return { value: 1 };
      case "blur":
        return { radius: 0, type: "gaussian" };
      default:
        return {};
    }
  };

  const effectsBridge = {
    isInitialized: vi.fn(() => true),
    applyVideoEffect: vi.fn(
      (clipId: string, effectType: string, params: Record<string, unknown> = {}) => {
        const effects = clipEffects.get(clipId) || [];
        const effect = {
          id: `effect-${effects.length + 1}`,
          type: effectType,
          enabled: true,
          params: { ...getDefaultParams(effectType), ...params },
          order: effects.length,
        };
        clipEffects.set(clipId, [...effects, effect]);
        return { success: true, effectId: effect.id };
      },
    ),
    getEffects: vi.fn((clipId: string) => [...(clipEffects.get(clipId) || [])]),
    getEffect: vi.fn((clipId: string, effectId: string) =>
      (clipEffects.get(clipId) || []).find((effect) => effect.id === effectId),
    ),
    deserializeEffects: vi.fn(
      (
        clipId: string,
        data: {
          effects: Array<{
            id: string;
            type: string;
            enabled: boolean;
            params: Record<string, unknown>;
            order: number;
          }>;
        },
      ) => {
        clipEffects.set(
          clipId,
          data.effects.map((effect) => ({ ...effect })),
        );
        return { success: true };
      },
    ),
    clearEffects: vi.fn((clipId: string) => {
      clipEffects.delete(clipId);
    }),
    getColorGrading: vi.fn(() => ({})),
  };

  const trackTransitions = new Map<string, Transition[]>();
  const transitionBridge = {
    isInitialized: vi.fn(() => true),
    setTransitionsForTrack: vi.fn(
      (trackId: string, transitions: Transition[]) => {
        trackTransitions.set(
          trackId,
          transitions.map((transition) => ({
            ...transition,
            params: { ...transition.params },
          })),
        );
      },
    ),
    clearTransitionsForTrack: vi.fn((trackId: string) => {
      trackTransitions.delete(trackId);
    }),
  };

  return {
    mockEffectsBridge: effectsBridge,
    mockEffectsBridgeState: { clipEffects },
    mockTransitionBridge: transitionBridge,
    mockTransitionBridgeState: { trackTransitions },
  };
});

vi.mock("../services/auto-save", () => ({
  autoSaveManager: {
    startAutoSave: vi.fn(),
    stopAutoSave: vi.fn(),
    triggerSave: vi.fn(),
    getRecentSaves: vi.fn().mockResolvedValue([]),
    loadSave: vi.fn(),
    deleteSave: vi.fn(),
  },
  initializeAutoSave: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../bridges/media-bridge", () => ({
  getMediaBridge: vi.fn(() => ({
    isInitialized: vi.fn().mockReturnValue(true),
    importFile: vi.fn().mockResolvedValue({
      success: true,
      media: {
        id: "mock-media-id",
        name: "test-video.mp4",
        type: "video",
        duration: 10,
        width: 1920,
        height: 1080,
        frameRate: 30,
      },
    }),
  })),
  initializeMediaBridge: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../bridges/effects-bridge", () => ({
  getEffectsBridge: vi.fn(() => mockEffectsBridge),
}));

vi.mock("../bridges/transition-bridge", () => ({
  getTransitionBridge: vi.fn(() => mockTransitionBridge),
}));

describe("ProjectStore", () => {
  beforeEach(() => {
    mockEffectsBridgeState.clipEffects.clear();
    mockTransitionBridgeState.trackTransitions.clear();
    useProjectStore.getState().createNewProject();
  });

  describe("project creation", () => {
    it("should create a new project with default settings", () => {
      const { project } = useProjectStore.getState();

      expect(project).toBeDefined();
      expect(project.name).toBeDefined();
      expect(project.name.length).toBeGreaterThan(0);
      expect(project.settings.width).toBe(1920);
      expect(project.settings.height).toBe(1080);
      expect(project.settings.frameRate).toBe(30);
    });

    it("should create project with custom name", () => {
      useProjectStore.getState().createNewProject("My Custom Project");
      const { project } = useProjectStore.getState();

      expect(project.name).toBe("My Custom Project");
    });

    it("should create project with custom settings", () => {
      useProjectStore.getState().createNewProject("4K Project", {
        width: 3840,
        height: 2160,
        frameRate: 60,
      });
      const { project } = useProjectStore.getState();

      expect(project.settings.width).toBe(3840);
      expect(project.settings.height).toBe(2160);
      expect(project.settings.frameRate).toBe(60);
    });

    it("should create project with empty timeline", () => {
      const { project } = useProjectStore.getState();

      expect(project.timeline).toBeDefined();
      expect(project.timeline.tracks).toBeDefined();
      expect(Array.isArray(project.timeline.tracks)).toBe(true);
    });

    it("should have unique project id", () => {
      const firstProject = useProjectStore.getState().project;
      useProjectStore.getState().createNewProject();
      const secondProject = useProjectStore.getState().project;

      expect(firstProject.id).not.toBe(secondProject.id);
    });

    it("should reset action history on new project", () => {
      const store = useProjectStore.getState();
      expect(store.canUndo()).toBe(false);
      expect(store.canRedo()).toBe(false);
    });
  });

  describe("project loading", () => {
    it("should load an existing project", () => {
      const existingProject: Project = {
        id: "existing-project-id",
        name: "Loaded Project",
        createdAt: Date.now() - 1000,
        modifiedAt: Date.now(),
        settings: {
          width: 1280,
          height: 720,
          frameRate: 24,
          sampleRate: 44100,
          channels: 2,
        },
        mediaLibrary: { items: [] },
        timeline: {
          tracks: [],
          subtitles: [],
          duration: 0,
          markers: [],
        },
      };

      useProjectStore.getState().loadProject(existingProject);
      const { project } = useProjectStore.getState();

      expect(project.id).toBe("existing-project-id");
      expect(project.name).toBe("Loaded Project");
      expect(project.settings.width).toBe(1280);
    });

    it("should preserve project data on load", () => {
      const mockMediaItem: MediaItem = {
        id: "media-1",
        name: "test.mp4",
        type: "video",
        fileHandle: null,
        blob: null,
        metadata: {
          duration: 30,
          width: 1920,
          height: 1080,
          frameRate: 30,
          codec: "h264",
          sampleRate: 48000,
          channels: 2,
          fileSize: 1000000,
        },
        thumbnailUrl: null,
        waveformData: null,
      };

      const projectWithMedia: Project = {
        id: "project-with-media",
        name: "Media Project",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: { items: [mockMediaItem] },
        timeline: {
          tracks: [
            {
              id: "track-1",
              type: "video",
              name: "Video 1",
              clips: [],
              transitions: [],
              locked: false,
              hidden: false,
              muted: false,
              solo: false,
            },
          ],
          subtitles: [],
          duration: 30,
          markers: [],
        },
      };

      useProjectStore.getState().loadProject(projectWithMedia);
      const { project } = useProjectStore.getState();

      expect(project.mediaLibrary.items.length).toBe(1);
      expect(project.mediaLibrary.items[0].name).toBe("test.mp4");
    });
  });

  describe("project renaming", () => {
    it("should rename project", async () => {
      const result = await useProjectStore.getState().renameProject("New Name");

      expect(result.success).toBe(true);

      const { project } = useProjectStore.getState();
      expect(project.name).toBe("New Name");
    });

    it("should preserve other project properties when renaming", async () => {
      const originalProject = useProjectStore.getState().project;
      const originalId = originalProject.id;
      const originalSettings = { ...originalProject.settings };

      await useProjectStore.getState().renameProject("Renamed Project");

      const { project } = useProjectStore.getState();
      expect(project.id).toBe(originalId);
      expect(project.settings).toEqual(originalSettings);
    });
  });

  describe("settings update", () => {
    it("should update project settings", async () => {
      const result = await useProjectStore.getState().updateSettings({
        width: 2560,
        height: 1440,
      });

      expect(result.success).toBe(true);

      const { project } = useProjectStore.getState();
      expect(project.settings.width).toBe(2560);
      expect(project.settings.height).toBe(1440);
    });

    it("should preserve unmodified settings", async () => {
      const originalFrameRate =
        useProjectStore.getState().project.settings.frameRate;
      const originalSampleRate =
        useProjectStore.getState().project.settings.sampleRate;

      await useProjectStore.getState().updateSettings({
        width: 3840,
        height: 2160,
      });

      const { project } = useProjectStore.getState();
      expect(project.settings.frameRate).toBe(originalFrameRate);
      expect(project.settings.sampleRate).toBe(originalSampleRate);
    });
  });

  describe("track operations", () => {
    it("should add a video track", async () => {
      const initialTrackCount =
        useProjectStore.getState().project.timeline.tracks.length;

      const result = await useProjectStore.getState().addTrack("video");

      expect(result.success).toBe(true);

      const newTrackCount =
        useProjectStore.getState().project.timeline.tracks.length;
      expect(newTrackCount).toBe(initialTrackCount + 1);
    });

    it("should add an audio track", async () => {
      const result = await useProjectStore.getState().addTrack("audio");
      expect(result.success).toBe(true);
    });

    it("should get track by id", async () => {
      await useProjectStore.getState().addTrack("video");
      const { project } = useProjectStore.getState();
      const trackId = project.timeline.tracks[0].id;

      const track = useProjectStore.getState().getTrack(trackId);
      expect(track).toBeDefined();
      expect(track?.id).toBe(trackId);
    });

    it("should return undefined for non-existent track", () => {
      const track = useProjectStore.getState().getTrack("non-existent-id");
      expect(track).toBeUndefined();
    });

    it("should lock a track", async () => {
      await useProjectStore.getState().addTrack("video");
      const { project } = useProjectStore.getState();
      const trackId = project.timeline.tracks[0].id;

      const result = await useProjectStore.getState().lockTrack(trackId, true);
      expect(result.success).toBe(true);

      const lockedTrack = useProjectStore.getState().getTrack(trackId);
      expect(lockedTrack?.locked).toBe(true);
    });

    it("should unlock a track", async () => {
      await useProjectStore.getState().addTrack("video");
      const { project } = useProjectStore.getState();
      const trackId = project.timeline.tracks[0].id;

      await useProjectStore.getState().lockTrack(trackId, true);
      await useProjectStore.getState().lockTrack(trackId, false);

      const unlockedTrack = useProjectStore.getState().getTrack(trackId);
      expect(unlockedTrack?.locked).toBe(false);
    });

    it("should mute a track", async () => {
      const { project } = useProjectStore.getState();
      const audioTrack = project.timeline.tracks.find(
        (t) => t.type === "audio",
      );

      if (audioTrack) {
        const result = await useProjectStore
          .getState()
          .muteTrack(audioTrack.id, true);
        expect(result.success).toBe(true);

        const mutedTrack = useProjectStore.getState().getTrack(audioTrack.id);
        expect(mutedTrack?.muted).toBe(true);
      }
    });

    it("should unmute a track", async () => {
      const { project } = useProjectStore.getState();
      const audioTrack = project.timeline.tracks.find(
        (t) => t.type === "audio",
      );

      if (audioTrack) {
        await useProjectStore.getState().muteTrack(audioTrack.id, true);
        await useProjectStore.getState().muteTrack(audioTrack.id, false);

        const unmutedTrack = useProjectStore.getState().getTrack(audioTrack.id);
        expect(unmutedTrack?.muted).toBe(false);
      }
    });

    it("should hide a track", async () => {
      await useProjectStore.getState().addTrack("video");
      const { project } = useProjectStore.getState();
      const trackId = project.timeline.tracks[0].id;

      const result = await useProjectStore.getState().hideTrack(trackId, true);
      expect(result.success).toBe(true);

      const hiddenTrack = useProjectStore.getState().getTrack(trackId);
      expect(hiddenTrack?.hidden).toBe(true);
    });

    it("should show a hidden track", async () => {
      await useProjectStore.getState().addTrack("video");
      const { project } = useProjectStore.getState();
      const trackId = project.timeline.tracks[0].id;

      await useProjectStore.getState().hideTrack(trackId, true);
      await useProjectStore.getState().hideTrack(trackId, false);

      const visibleTrack = useProjectStore.getState().getTrack(trackId);
      expect(visibleTrack?.hidden).toBe(false);
    });
  });

  describe("media operations", () => {
    it("should get media item by id", () => {
      const projectWithMedia: Project = {
        id: "test-project",
        name: "Test",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: {
          items: [
            {
              id: "media-123",
              name: "video.mp4",
              type: "video",
              fileHandle: null,
              blob: null,
              metadata: {
                duration: 10,
                width: 1920,
                height: 1080,
                frameRate: 30,
                codec: "h264",
                sampleRate: 48000,
                channels: 2,
                fileSize: 500000,
              },
              thumbnailUrl: null,
              waveformData: null,
            },
          ],
        },
        timeline: {
          tracks: [],
          subtitles: [],
          duration: 0,
          markers: [],
        },
      };

      useProjectStore.getState().loadProject(projectWithMedia);

      const media = useProjectStore.getState().getMediaItem("media-123");
      expect(media).toBeDefined();
      expect(media?.name).toBe("video.mp4");
    });

    it("should return undefined for non-existent media", () => {
      const media = useProjectStore.getState().getMediaItem("non-existent");
      expect(media).toBeUndefined();
    });
  });

  describe("timeline duration", () => {
    it("should calculate timeline duration from clips", () => {
      const projectWithClips: Project = {
        id: "test-project",
        name: "Test",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: { items: [] },
        timeline: {
          tracks: [
            {
              id: "track-1",
              type: "video",
              name: "Video",
              clips: [
                {
                  id: "clip-1",
                  mediaId: "media-1",
                  trackId: "track-1",
                  startTime: 0,
                  duration: 10,
                  inPoint: 0,
                  outPoint: 10,
                  effects: [],
                  audioEffects: [],
                  transform: {
                    position: { x: 0.5, y: 0.5 },
                    scale: { x: 1, y: 1 },
                    rotation: 0,
                    anchor: { x: 0.5, y: 0.5 },
                    opacity: 1,
                  },
                  volume: 1,
                  keyframes: [],
                },
                {
                  id: "clip-2",
                  mediaId: "media-2",
                  trackId: "track-1",
                  startTime: 10,
                  duration: 5,
                  inPoint: 0,
                  outPoint: 5,
                  effects: [],
                  audioEffects: [],
                  transform: {
                    position: { x: 0.5, y: 0.5 },
                    scale: { x: 1, y: 1 },
                    rotation: 0,
                    anchor: { x: 0.5, y: 0.5 },
                    opacity: 1,
                  },
                  volume: 1,
                  keyframes: [],
                },
              ],
              transitions: [],
              locked: false,
              hidden: false,
              muted: false,
              solo: false,
            },
          ],
          subtitles: [],
          duration: 15,
          markers: [],
        },
      };

      useProjectStore.getState().loadProject(projectWithClips);

      const duration = useProjectStore.getState().getTimelineDuration();
      expect(duration).toBe(15);
    });
  });

  describe("video effects", () => {
    const createProjectWithVideoClip = (): Project => ({
      id: "effects-project",
      name: "Effects Project",
      createdAt: Date.now(),
      modifiedAt: Date.now(),
      settings: {
        width: 1920,
        height: 1080,
        frameRate: 30,
        sampleRate: 48000,
        channels: 2,
      },
      mediaLibrary: { items: [] },
      timeline: {
        tracks: [
          {
            id: "video-track-1",
            type: "video",
            name: "Video",
            clips: [
              {
                id: "video-clip-1",
                mediaId: "video-media-1",
                trackId: "video-track-1",
                startTime: 0,
                duration: 8,
                inPoint: 0,
                outPoint: 8,
                effects: [],
                audioEffects: [],
                transform: {
                  position: { x: 0.5, y: 0.5 },
                  scale: { x: 1, y: 1 },
                  rotation: 0,
                  anchor: { x: 0.5, y: 0.5 },
                  opacity: 1,
                },
                volume: 1,
                keyframes: [],
              },
            ],
            transitions: [],
            locked: false,
            hidden: false,
            muted: false,
            solo: false,
          },
        ],
        subtitles: [],
        duration: 8,
        markers: [],
      },
    });

    it("should persist video effects to the clip timeline state", () => {
      useProjectStore.getState().loadProject(createProjectWithVideoClip());

      const addedEffect = useProjectStore
        .getState()
        .addVideoEffect("video-clip-1", "brightness", { value: 15 });

      expect(addedEffect).not.toBeNull();
      expect(useProjectStore.getState().getClip("video-clip-1")?.effects).toEqual([
        {
          id: addedEffect!.id,
          type: "brightness",
          enabled: true,
          params: { value: 15 },
        },
      ]);
      expect(useProjectStore.getState().getVideoEffects("video-clip-1")).toHaveLength(1);
    });

    it("should keep clip effects synchronized across update, toggle, reorder, and remove", () => {
      useProjectStore.getState().loadProject(createProjectWithVideoClip());

      const brightness = useProjectStore
        .getState()
        .addVideoEffect("video-clip-1", "brightness", { value: 10 });
      const contrast = useProjectStore
        .getState()
        .addVideoEffect("video-clip-1", "contrast", { value: 1.2 });

      expect(brightness).not.toBeNull();
      expect(contrast).not.toBeNull();

      const updated = useProjectStore
        .getState()
        .updateVideoEffect("video-clip-1", brightness!.id, { value: 20 });
      const toggled = useProjectStore
        .getState()
        .toggleVideoEffect("video-clip-1", brightness!.id, false);
      const reordered = useProjectStore
        .getState()
        .reorderVideoEffects("video-clip-1", [contrast!.id, brightness!.id]);
      const removed = useProjectStore
        .getState()
        .removeVideoEffect("video-clip-1", contrast!.id);

      expect(updated?.params).toEqual({ value: 20 });
      expect(toggled?.enabled).toBe(false);
      expect(reordered).toBe(true);
      expect(removed).toBe(true);
      expect(useProjectStore.getState().getClip("video-clip-1")?.effects).toEqual([
        {
          id: brightness!.id,
          type: "brightness",
          enabled: false,
          params: { value: 20 },
        },
      ]);
    });
  });

  describe("editing templates", () => {
    const createProjectWithEditableClip = (): Project => {
      const mediaItem: MediaItem = {
        id: "video-media-1",
        name: "hero-shot.mp4",
        type: "video",
        fileHandle: null,
        blob: null,
        metadata: {
          duration: 10,
          width: 1920,
          height: 1080,
          frameRate: 30,
          codec: "h264",
          sampleRate: 48000,
          channels: 2,
          fileSize: 1000000,
        },
        thumbnailUrl: null,
        waveformData: null,
      };

      const clip: Clip = {
        id: "video-clip-1",
        mediaId: mediaItem.id,
        trackId: "video-track-1",
        startTime: 0,
        duration: 10,
        inPoint: 0,
        outPoint: 10,
        effects: [],
        audioEffects: [],
        transform: {
          position: { x: 0.5, y: 0.5 },
          scale: { x: 1, y: 1 },
          rotation: 0,
          anchor: { x: 0.5, y: 0.5 },
          opacity: 1,
        },
        volume: 1,
        keyframes: [],
      };

      return {
        id: "editing-template-project",
        name: "Editing Template Project",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: { items: [mediaItem] },
        timeline: {
          tracks: [
            {
              id: "video-track-1",
              type: "video",
              name: "Video",
              clips: [clip],
              transitions: [],
              locked: false,
              hidden: false,
              muted: false,
              solo: false,
            },
          ],
          subtitles: [],
          duration: 10,
          markers: [],
        },
      };
    };

    beforeEach(() => {
      const titleEngine = useEngineStore.getState().getTitleEngine();
      const graphicsEngine = useEngineStore.getState().getGraphicsEngine();

      titleEngine?.loadTextClips([]);
      graphicsEngine?.loadShapeClips([]);
      graphicsEngine?.loadSVGClips([]);
      graphicsEngine?.loadStickerClips([]);
    });

    const getRecipeTextValues = (applicationId: string): string[] =>
      (useEngineStore.getState().getTitleEngine()?.getAllTextClips() || [])
        .filter(
          (clip) => clip.metadata?.templateSource?.applicationId === applicationId,
        )
        .map((clip) => clip.text)
        .sort();

    it("applies a clip-scoped recipe and records metadata plus overlays", () => {
      useProjectStore.getState().loadProject(createProjectWithEditableClip());

      const applicationId = useProjectStore.getState().applyEditingTemplate(
        "branding-lower-third",
        "video-clip-1",
        {
          name: "Ada Lovelace",
          role: "Director",
        },
      );

      expect(applicationId).toBeTruthy();

      const clip = useProjectStore.getState().getClip("video-clip-1");
      expect(clip?.metadata?.appliedTemplates).toEqual([
        expect.objectContaining({
          templateId: "branding-lower-third",
          applicationId,
          controlValues: expect.objectContaining({
            name: "Ada Lovelace",
            role: "Director",
          }),
        }),
      ]);
      expect(
        useProjectStore
          .getState()
          .project.timeline.tracks.map((track) => track.type),
      ).toEqual(["video", "graphics", "text"]);
      expect(
        useEngineStore.getState().getGraphicsEngine()?.getAllShapeClips(),
      ).toHaveLength(1);
      expect(
        useEngineStore.getState().getTitleEngine()?.getAllTextClips(),
      ).toHaveLength(2);
    });

    it("removes a clip-scoped recipe and cleans up its generated tracks", () => {
      useProjectStore.getState().loadProject(createProjectWithEditableClip());

      const applicationId = useProjectStore.getState().applyEditingTemplate(
        "branding-lower-third",
        "video-clip-1",
      );

      expect(applicationId).toBeTruthy();
      expect(
        useProjectStore.getState().removeEditingTemplateApplication(
          "video-clip-1",
          applicationId!,
        ),
      ).toBe(true);

      const clip = useProjectStore.getState().getClip("video-clip-1");
      expect(clip?.metadata?.appliedTemplates || []).toHaveLength(0);
      expect(
        useProjectStore
          .getState()
          .project.timeline.tracks.map((track) => track.type),
      ).toEqual(["video"]);
      expect(
        useEngineStore.getState().getGraphicsEngine()?.getAllShapeClips(),
      ).toHaveLength(0);
      expect(
        useEngineStore.getState().getTitleEngine()?.getAllTextClips(),
      ).toHaveLength(0);
    });

    it("updates an applied recipe in place and keeps its application id", () => {
      useProjectStore.getState().loadProject(createProjectWithEditableClip());

      const applicationId = useProjectStore.getState().applyEditingTemplate(
        "branding-lower-third",
        "video-clip-1",
        {
          name: "Ada Lovelace",
          role: "Director",
        },
      );

      expect(applicationId).toBeTruthy();
      expect(
        useProjectStore.getState().updateEditingTemplateApplication(
          "video-clip-1",
          applicationId!,
          {
            name: "Grace Hopper",
            role: "Engineer",
          },
        ),
      ).toBe(true);

      const clip = useProjectStore.getState().getClip("video-clip-1");
      expect(clip?.metadata?.appliedTemplates).toEqual([
        expect.objectContaining({
          applicationId,
          controlValues: expect.objectContaining({
            name: "Grace Hopper",
            role: "Engineer",
          }),
        }),
      ]);
      expect(getRecipeTextValues(applicationId!)).toEqual([
        "Engineer",
        "Grace Hopper",
      ]);
      expect(
        useEngineStore
          .getState()
          .getGraphicsEngine()
          ?.getAllShapeClips()
          .filter(
            (clip) => clip.metadata?.templateSource?.applicationId === applicationId,
          ),
      ).toHaveLength(1);
    });

    it("undos and redoes a recipe update between previous and current controls", async () => {
      useProjectStore.getState().loadProject(createProjectWithEditableClip());

      const applicationId = useProjectStore.getState().applyEditingTemplate(
        "branding-lower-third",
        "video-clip-1",
        {
          name: "Ada Lovelace",
          role: "Director",
        },
      );

      expect(applicationId).toBeTruthy();
      expect(
        useProjectStore.getState().updateEditingTemplateApplication(
          "video-clip-1",
          applicationId!,
          {
            name: "Grace Hopper",
            role: "Engineer",
          },
        ),
      ).toBe(true);
      expect(getRecipeTextValues(applicationId!)).toEqual([
        "Engineer",
        "Grace Hopper",
      ]);

      await useProjectStore.getState().undo();
      expect(getRecipeTextValues(applicationId!)).toEqual([
        "Ada Lovelace",
        "Director",
      ]);
      expect(
        useProjectStore.getState().getClip("video-clip-1")?.metadata?.appliedTemplates,
      ).toEqual([
        expect.objectContaining({
          applicationId,
          controlValues: expect.objectContaining({
            name: "Ada Lovelace",
            role: "Director",
          }),
        }),
      ]);

      await useProjectStore.getState().redo();
      expect(getRecipeTextValues(applicationId!)).toEqual([
        "Engineer",
        "Grace Hopper",
      ]);
      expect(
        useProjectStore.getState().getClip("video-clip-1")?.metadata?.appliedTemplates,
      ).toEqual([
        expect.objectContaining({
          applicationId,
          controlValues: expect.objectContaining({
            name: "Grace Hopper",
            role: "Engineer",
          }),
        }),
      ]);
    });

    it("undos newer timeline actions before undoing a recipe and can redo the recipe", async () => {
      useProjectStore.getState().loadProject(createProjectWithEditableClip());

      const applicationId = useProjectStore.getState().applyEditingTemplate(
        "branding-lower-third",
        "video-clip-1",
      );

      expect(applicationId).toBeTruthy();
      expect(useProjectStore.getState().project.timeline.tracks).toHaveLength(3);

      await useProjectStore.getState().addTrack("audio");
      expect(useProjectStore.getState().project.timeline.tracks).toHaveLength(4);

      await useProjectStore.getState().undo();
      expect(useProjectStore.getState().project.timeline.tracks).toHaveLength(3);
      expect(
        useProjectStore.getState().getClip("video-clip-1")?.metadata?.appliedTemplates,
      ).toHaveLength(1);

      await useProjectStore.getState().undo();
      expect(
        useProjectStore.getState().getClip("video-clip-1")?.metadata?.appliedTemplates || [],
      ).toHaveLength(0);
      expect(
        useEngineStore.getState().getGraphicsEngine()?.getAllShapeClips(),
      ).toHaveLength(0);
      expect(
        useEngineStore.getState().getTitleEngine()?.getAllTextClips(),
      ).toHaveLength(0);

      await useProjectStore.getState().redo();
      expect(
        useProjectStore.getState().getClip("video-clip-1")?.metadata?.appliedTemplates,
      ).toHaveLength(1);
      expect(
        useEngineStore.getState().getGraphicsEngine()?.getAllShapeClips(),
      ).toHaveLength(1);
      expect(
        useEngineStore.getState().getTitleEngine()?.getAllTextClips(),
      ).toHaveLength(2);
    });
  });

  describe("clip transitions", () => {
    const createProjectWithAdjacentClips = (): Project => ({
      id: "transition-project",
      name: "Transition Project",
      createdAt: Date.now(),
      modifiedAt: Date.now(),
      settings: {
        width: 1920,
        height: 1080,
        frameRate: 30,
        sampleRate: 48000,
        channels: 2,
      },
      mediaLibrary: { items: [] },
      timeline: {
        tracks: [
          {
            id: "video-track-1",
            type: "video",
            name: "Video",
            clips: [
              {
                id: "clip-a",
                mediaId: "video-a",
                trackId: "video-track-1",
                startTime: 0,
                duration: 4,
                inPoint: 0,
                outPoint: 4,
                effects: [],
                audioEffects: [],
                transform: {
                  position: { x: 0.5, y: 0.5 },
                  scale: { x: 1, y: 1 },
                  rotation: 0,
                  anchor: { x: 0.5, y: 0.5 },
                  opacity: 1,
                },
                volume: 1,
                keyframes: [],
              },
              {
                id: "clip-b",
                mediaId: "video-b",
                trackId: "video-track-1",
                startTime: 4,
                duration: 4,
                inPoint: 0,
                outPoint: 4,
                effects: [],
                audioEffects: [],
                transform: {
                  position: { x: 0.5, y: 0.5 },
                  scale: { x: 1, y: 1 },
                  rotation: 0,
                  anchor: { x: 0.5, y: 0.5 },
                  opacity: 1,
                },
                volume: 1,
                keyframes: [],
              },
            ],
            transitions: [],
            locked: false,
            hidden: false,
            muted: false,
            solo: false,
          },
        ],
        subtitles: [],
        duration: 8,
        markers: [],
      },
    });

    it("should persist adjacent clip transitions and mirror them into the transition bridge", () => {
      useProjectStore.getState().loadProject(createProjectWithAdjacentClips());

      const transition: Transition = {
        id: "transition-1",
        clipAId: "clip-a",
        clipBId: "clip-b",
        type: "crossfade",
        duration: 0.5,
        params: { curve: "ease" },
      };

      const addedTransition = useProjectStore
        .getState()
        .addClipTransition(transition);
      const updatedTransition = useProjectStore
        .getState()
        .updateClipTransition("transition-1", {
          duration: 0.75,
          params: { curve: "linear" },
        });

      expect(addedTransition).toEqual(transition);
      expect(useProjectStore.getState().getClipTransitionBetweenClips("clip-a", "clip-b")).toEqual({
        id: "transition-1",
        clipAId: "clip-a",
        clipBId: "clip-b",
        type: "crossfade",
        duration: 0.75,
        params: { curve: "linear" },
      });
      expect(updatedTransition).toEqual({
        id: "transition-1",
        clipAId: "clip-a",
        clipBId: "clip-b",
        type: "crossfade",
        duration: 0.75,
        params: { curve: "linear" },
      });
      expect(mockTransitionBridgeState.trackTransitions.get("video-track-1")).toEqual([
        {
          id: "transition-1",
          clipAId: "clip-a",
          clipBId: "clip-b",
          type: "crossfade",
          duration: 0.75,
          params: { curve: "linear" },
        },
      ]);

      const removedTransition = useProjectStore
        .getState()
        .removeClipTransition("transition-1");

      expect(removedTransition).toBe(true);
      expect(useProjectStore.getState().getClipTransition("transition-1")).toBeUndefined();
      expect(mockTransitionBridgeState.trackTransitions.get("video-track-1")).toEqual([]);
    });
  });

  describe("marker operations", () => {
    it("should add a marker", () => {
      useProjectStore.getState().addMarker(5, "Scene 1", "#ff0000");

      const markers = useProjectStore.getState().getMarkers();
      expect(markers.length).toBe(1);
      expect(markers[0].time).toBe(5);
      expect(markers[0].label).toBe("Scene 1");
    });

    it("should remove a marker", () => {
      useProjectStore.getState().addMarker(5, "Scene 1");
      const markers = useProjectStore.getState().getMarkers();
      const markerId = markers[0].id;

      useProjectStore.getState().removeMarker(markerId);

      const updatedMarkers = useProjectStore.getState().getMarkers();
      expect(updatedMarkers.length).toBe(0);
    });

    it("should get marker by id", () => {
      useProjectStore.getState().addMarker(10, "Marker Test");
      const markers = useProjectStore.getState().getMarkers();
      const markerId = markers[0].id;

      const marker = useProjectStore.getState().getMarker(markerId);
      expect(marker).toBeDefined();
      expect(marker?.time).toBe(10);
    });
  });

  describe("undo/redo", () => {
    it("should not be able to undo without actions", () => {
      expect(useProjectStore.getState().canUndo()).toBe(false);
    });

    it("should not be able to redo without undone actions", () => {
      expect(useProjectStore.getState().canRedo()).toBe(false);
    });

    it("should be able to undo after an action", async () => {
      await useProjectStore.getState().addTrack("video");
      expect(useProjectStore.getState().canUndo()).toBe(true);
    });
  });

  describe("clipboard operations", () => {
    it("should start with empty clipboard", () => {
      expect(useProjectStore.getState().clipboard).toEqual([]);
    });

    it("should copy clips to clipboard", () => {
      const mockClip: Clip = {
        id: "clip-to-copy",
        mediaId: "media-1",
        trackId: "track-1",
        startTime: 0,
        duration: 5,
        inPoint: 0,
        outPoint: 5,
        effects: [],
        audioEffects: [],
        transform: {
          position: { x: 0.5, y: 0.5 },
          scale: { x: 1, y: 1 },
          rotation: 0,
          anchor: { x: 0.5, y: 0.5 },
          opacity: 1,
        },
        volume: 1,
        keyframes: [],
      };

      const projectWithClip: Project = {
        id: "test",
        name: "Test",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: { items: [] },
        timeline: {
          tracks: [
            {
              id: "track-1",
              type: "video",
              name: "Video",
              clips: [mockClip],
              transitions: [],
              locked: false,
              hidden: false,
              muted: false,
              solo: false,
            },
          ],
          subtitles: [],
          duration: 5,
          markers: [],
        },
      };

      useProjectStore.getState().loadProject(projectWithClip);
      useProjectStore.getState().copyClips(["clip-to-copy"]);

      expect(useProjectStore.getState().clipboard.length).toBe(1);
    });
  });

  describe("separateAudio", () => {
    const createProjectWithVideoClip = (audioTrackCount?: number): Project => {
      const mediaItem: MediaItem = {
        id: "video-media-1",
        name: "multi-audio.mp4",
        type: "video",
        fileHandle: null,
        blob: null,
        metadata: {
          duration: 10,
          width: 1920,
          height: 1080,
          frameRate: 30,
          codec: "h264",
          sampleRate: 48000,
          channels: 2,
          fileSize: 1000000,
          audioTrackCount,
        },
        thumbnailUrl: null,
        waveformData: null,
      };

      const videoClip: Clip = {
        id: "video-clip-1",
        mediaId: "video-media-1",
        trackId: "video-track-1",
        startTime: 0,
        duration: 10,
        inPoint: 0,
        outPoint: 10,
        effects: [],
        audioEffects: [],
        transform: {
          position: { x: 0.5, y: 0.5 },
          scale: { x: 1, y: 1 },
          rotation: 0,
          anchor: { x: 0.5, y: 0.5 },
          opacity: 1,
        },
        volume: 1,
        keyframes: [],
      };

      return {
        id: "test-project",
        name: "Test",
        createdAt: Date.now(),
        modifiedAt: Date.now(),
        settings: {
          width: 1920,
          height: 1080,
          frameRate: 30,
          sampleRate: 48000,
          channels: 2,
        },
        mediaLibrary: { items: [mediaItem] },
        timeline: {
          tracks: [
            {
              id: "video-track-1",
              type: "video",
              name: "Video",
              clips: [videoClip],
              transitions: [],
              locked: false,
              hidden: false,
              muted: false,
              solo: false,
            },
          ],
          subtitles: [],
          duration: 10,
          markers: [],
        },
      };
    };

    it("should create one audio clip when media has a single audio track", async () => {
      useProjectStore.getState().loadProject(createProjectWithVideoClip(1));
      const result = await useProjectStore.getState().separateAudio("video-clip-1");

      expect(result.success).toBe(true);
      const { project } = useProjectStore.getState();
      const audioTracks = project.timeline.tracks.filter((t) => t.type === "audio");
      expect(audioTracks.length).toBe(1);
      expect(audioTracks[0].clips.length).toBe(1);
      expect(audioTracks[0].clips[0].mediaId).toBe("video-media-1");
    });

    it("should create multiple audio clips when media has multiple audio tracks", async () => {
      useProjectStore.getState().loadProject(createProjectWithVideoClip(3));
      const result = await useProjectStore.getState().separateAudio("video-clip-1");

      expect(result.success).toBe(true);
      const { project } = useProjectStore.getState();
      const audioTracks = project.timeline.tracks.filter((t) => t.type === "audio");
      expect(audioTracks.length).toBe(3);

      // Each audio track should have one clip with the correct audioTrackIndex
      for (let i = 0; i < 3; i++) {
        expect(audioTracks[i].clips.length).toBe(1);
        expect(audioTracks[i].clips[0].mediaId).toBe("video-media-1");
        expect(audioTracks[i].clips[0].audioTrackIndex).toBe(i);
      }
    });

    it("should default to one audio track when audioTrackCount is undefined", async () => {
      useProjectStore.getState().loadProject(createProjectWithVideoClip(undefined));
      const result = await useProjectStore.getState().separateAudio("video-clip-1");

      expect(result.success).toBe(true);
      const { project } = useProjectStore.getState();
      const audioTracks = project.timeline.tracks.filter((t) => t.type === "audio");
      expect(audioTracks.length).toBe(1);
    });

    it("should return an error when clip is not found", async () => {
      useProjectStore.getState().createNewProject();
      const result = await useProjectStore.getState().separateAudio("non-existent-clip");

      expect(result.success).toBe(false);
      expect(result.error?.code).toBe("CLIP_NOT_FOUND");
    });
  });
});

describe("ProjectStore - Text Clips", () => {
  beforeEach(async () => {
    useProjectStore.getState().createNewProject();
    await useProjectStore.getState().addTrack("text");
  });

  it("should create a text clip", async () => {
    const { project } = useProjectStore.getState();
    const trackId = project.timeline.tracks[0].id;

    const textClip = useProjectStore
      .getState()
      .createTextClip(trackId, 0, "Hello World", 5);

    expect(textClip).toBeDefined();
    expect(textClip?.text).toBe("Hello World");
    expect(textClip?.duration).toBe(5);
  });

  it("should get all text clips", async () => {
    const { project } = useProjectStore.getState();
    const trackId = project.timeline.tracks[0].id;

    const initialCount = useProjectStore.getState().getAllTextClips().length;

    useProjectStore.getState().createTextClip(trackId, 0, "First", 3);
    useProjectStore.getState().createTextClip(trackId, 3, "Second", 3);

    const allTextClips = useProjectStore.getState().getAllTextClips();
    expect(allTextClips.length).toBe(initialCount + 2);
  });

  it("should get available animation presets", () => {
    const presets = useProjectStore.getState().getAvailableAnimationPresets();
    expect(Array.isArray(presets)).toBe(true);
  });
});

describe("ProjectStore - Subtitles (consolidated into text clips)", () => {
  beforeEach(() => {
    useProjectStore.getState().createNewProject();
  });

  it.skip("should add a subtitle - skipped: subtitles consolidated into text clips", () => {
    // Subtitles are now created as text clips on a Captions track
    // The addSubtitle function creates text clips, but getSubtitle reads from the old subtitles array
    // This test is skipped until the API is fully migrated
  });

  it.skip("should remove a subtitle - skipped: subtitles consolidated into text clips", () => {
    // Subtitles are now created as text clips on a Captions track
  });

  it.skip("should update a subtitle - skipped: subtitles consolidated into text clips", () => {
    // Subtitles are now created as text clips on a Captions track
  });

  it.skip("should export SRT - skipped: subtitles consolidated into text clips", () => {
    // SRT export now uses text clips from Captions track
  });

  it("should get subtitle style presets", async () => {
    const presets = await useProjectStore.getState().getSubtitleStylePresets();
    expect(Array.isArray(presets)).toBe(true);
  });
});
