import { describe, expect, it } from "vitest";
import { uploadReducer, createInitialUploadState } from "./upload-reducer";

const reduce = (state: uploadReducer.State, action: uploadReducer.Action) =>
  uploadReducer(state, action);

const createState = (
  overrides: Partial<uploadReducer.State> = {}
): uploadReducer.State => ({
  ...createInitialUploadState(),
  ...overrides,
});

const createYouTubeEntry = (
  overrides: Partial<Omit<uploadReducer.YouTubeUploadEntry, "uploadType">> = {}
): uploadReducer.YouTubeUploadEntry => ({
  uploadId: "upload-1",
  videoId: "video-1",
  title: "Test Video",
  progress: 0,
  status: "uploading",
  uploadType: "youtube",
  youtubeVideoId: null,
  errorMessage: null,
  retryCount: 0,
  terminal: false,
  dependsOn: null,
  parentUploadId: null,
  ...overrides,
});

const createBufferEntry = (
  overrides: Partial<Omit<uploadReducer.BufferUploadEntry, "uploadType">> = {}
): uploadReducer.BufferUploadEntry => ({
  uploadId: "upload-1",
  videoId: "video-1",
  title: "Test Video",
  progress: 0,
  status: "uploading",
  uploadType: "buffer",
  bufferStage: "uploading-blob",
  errorMessage: null,
  retryCount: 0,
  terminal: false,
  dependsOn: null,
  parentUploadId: null,
  ...overrides,
});

const createExportEntry = (
  overrides: Partial<Omit<uploadReducer.ExportUploadEntry, "uploadType">> = {}
): uploadReducer.ExportUploadEntry => ({
  uploadId: "upload-1",
  videoId: "video-1",
  title: "Test Video",
  progress: 0,
  status: "uploading",
  uploadType: "export",
  exportStage: "queued",
  isBatchEntry: false,
  videoUploadStage: null,
  uploadedBytes: 0,
  totalBytes: null,
  errorMessage: null,
  retryCount: 0,
  terminal: false,
  dependsOn: null,
  parentUploadId: null,
  ...overrides,
});

const createPublishEntry = (
  overrides: Partial<Omit<uploadReducer.PublishUploadEntry, "uploadType">> = {}
): uploadReducer.PublishUploadEntry => ({
  uploadId: "upload-1",
  videoId: "",
  title: "My Course",
  progress: 0,
  status: "uploading",
  uploadType: "publish",
  publishStage: "validating",
  newDraftVersionId: null,
  courseId: "course-1",
  errorMessage: null,
  retryCount: 0,
  terminal: false,
  dependsOn: null,
  parentUploadId: null,
  ...overrides,
});

describe("UPDATE_BUFFER_STAGE", () => {
  it("should update buffer stage for existing upload", () => {
    const state = reduce(
      createState({
        uploads: {
          "upload-1": createBufferEntry({ bufferStage: "uploading-blob" }),
        },
      }),
      {
        type: "UPDATE_BUFFER_STAGE",
        uploadId: "upload-1",
        stage: "creating-post",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "buffer" && upload.bufferStage).toBe(
      "creating-post"
    );
  });

  it("should transition from creating-post to polling", () => {
    const state = reduce(
      createState({
        uploads: {
          "upload-1": createBufferEntry({ bufferStage: "creating-post" }),
        },
      }),
      {
        type: "UPDATE_BUFFER_STAGE",
        uploadId: "upload-1",
        stage: "polling",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "buffer" && upload.bufferStage).toBe(
      "polling"
    );
  });

  it("should set progress based on stage", () => {
    const state = reduce(
      createState({
        uploads: {
          "upload-1": createBufferEntry({ bufferStage: "uploading-blob" }),
        },
      }),
      {
        type: "UPDATE_BUFFER_STAGE",
        uploadId: "upload-1",
        stage: "polling",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.progress).toBe(70);
  });

  it("should not modify state for non-existent upload", () => {
    const initial = createState();
    const state = reduce(initial, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "non-existent",
      stage: "creating-post",
    });

    expect(state).toBe(initial);
  });

  it("should not modify state for non-buffer upload", () => {
    const initial = createState({
      uploads: { "upload-1": createYouTubeEntry() },
    });
    const state = reduce(initial, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "upload-1",
      stage: "creating-post",
    });

    expect(state).toBe(initial);
  });
});

describe("UPDATE_EXPORT_STAGE", () => {
  it("should update export stage for existing upload", () => {
    const state = reduce(
      createState({
        uploads: {
          "upload-1": createExportEntry({
            exportStage: "concatenating-clips",
          }),
        },
      }),
      {
        type: "UPDATE_EXPORT_STAGE",
        uploadId: "upload-1",
        stage: "normalizing-audio",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "export" && upload.exportStage).toBe(
      "normalizing-audio"
    );
  });

  it("should update progress to the stage's band start", () => {
    let state = createState({
      uploads: { "upload-1": createExportEntry() },
    });

    // Band start: real ffmpeg progress fills 0→80 during concatenation.
    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "upload-1",
      stage: "concatenating-clips",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(0);

    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "upload-1",
      stage: "normalizing-audio",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(80);
  });

  it("should update export stage to queued", () => {
    const state = reduce(
      createState({
        uploads: {
          "upload-1": createExportEntry({
            exportStage: "concatenating-clips",
          }),
        },
      }),
      {
        type: "UPDATE_EXPORT_STAGE",
        uploadId: "upload-1",
        stage: "queued",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "export" && upload.exportStage).toBe("queued");
  });

  it("should not modify state for non-existent upload", () => {
    const initial = createState();
    const state = reduce(initial, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "non-existent",
      stage: "normalizing-audio",
    });

    expect(state).toBe(initial);
  });

  it("should not modify state for non-export upload", () => {
    const initial = createState({
      uploads: { "upload-1": createYouTubeEntry() },
    });
    const state = reduce(initial, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "upload-1",
      stage: "normalizing-audio",
    });

    expect(state).toBe(initial);
  });
});

describe("UPDATE_EXPORT_PROGRESS", () => {
  it("bands concatenation percent into 0–80", () => {
    let state = createState({
      uploads: { "upload-1": createExportEntry() },
    });

    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "concatenating-clips",
      percent: 50,
    });
    expect(state.uploads["upload-1"]!.progress).toBe(40);

    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "concatenating-clips",
      percent: 99,
    });
    expect(state.uploads["upload-1"]!.progress).toBe(79);
  });

  it("bands normalization percent into 80–99", () => {
    let state = createState({
      uploads: { "upload-1": createExportEntry() },
    });

    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "normalizing-audio",
      percent: 0,
    });
    expect(state.uploads["upload-1"]!.progress).toBe(80);

    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "normalizing-audio",
      percent: 99,
    });
    expect(state.uploads["upload-1"]!.progress).toBe(98);
  });

  it("never moves the bar backwards across late or out-of-order events", () => {
    let state = createState({
      uploads: { "upload-1": createExportEntry() },
    });

    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "upload-1",
      stage: "normalizing-audio",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(80);

    // A straggler event from the concatenation phase must not drag it back.
    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "concatenating-clips",
      percent: 50,
    });
    expect(state.uploads["upload-1"]!.progress).toBe(80);
  });

  it("updates the export stage alongside the percent", () => {
    let state = createState({
      uploads: { "upload-1": createExportEntry() },
    });

    state = reduce(state, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "concatenating-clips",
      percent: 10,
    });
    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "export" && upload.exportStage).toBe(
      "concatenating-clips"
    );
  });

  it("should not modify state for non-export upload", () => {
    const initial = createState({
      uploads: { "upload-1": createYouTubeEntry() },
    });
    const state = reduce(initial, {
      type: "UPDATE_EXPORT_PROGRESS",
      uploadId: "upload-1",
      stage: "concatenating-clips",
      percent: 50,
    });

    expect(state).toBe(initial);
  });
});

describe("UPDATE_PUBLISH_STAGE", () => {
  it("should update publish stage for existing publish upload", () => {
    const state = reduce(
      createState({
        uploads: { "upload-1": createPublishEntry() },
      }),
      {
        type: "UPDATE_PUBLISH_STAGE",
        uploadId: "upload-1",
        stage: "uploading",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "publish" && upload.publishStage).toBe(
      "uploading"
    );
  });

  it("should update progress based on publish stage", () => {
    let state = createState({
      uploads: { "upload-1": createPublishEntry() },
    });

    // Only the prologue is banded: validate, then Submit. Everything past it
    // overlaps, so the bar parks at the work band's floor and is filled by the
    // per-Video children instead.
    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "validating",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(2);

    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "freezing",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(4);

    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "cloning",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(6);

    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "exporting",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(10);

    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "uploading",
    });
    expect(state.uploads["upload-1"]!.progress).toBe(10);
  });

  // The Publish service emits `complete` as its last stage, one step before
  // the `complete` event settles the job. The client union used to omit it, so
  // the band lookup gave undefined and every successful Publish ended in
  // "Cannot read properties of undefined (reading 'start')".
  it("should accept the terminal complete stage", () => {
    const state = reduce(
      createState({
        uploads: { "upload-1": createPublishEntry() },
      }),
      {
        type: "UPDATE_PUBLISH_STAGE",
        uploadId: "upload-1",
        stage: "complete",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "publish" && upload.publishStage).toBe(
      "complete"
    );
    expect(upload.progress).toBe(99);
  });

  it("should not modify state for non-existent upload", () => {
    const initial = createState();
    const state = reduce(initial, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "non-existent",
      stage: "uploading",
    });

    expect(state).toBe(initial);
  });

  it("should not modify state for non-publish upload", () => {
    const initial = createState({
      uploads: { "upload-1": createYouTubeEntry() },
    });
    const state = reduce(initial, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "upload-1",
      stage: "uploading",
    });

    expect(state).toBe(initial);
  });
});

describe("PUBLISH_COMPLETE", () => {
  it("should set newDraftVersionId on publish entry", () => {
    const state = reduce(
      createState({
        uploads: { "upload-1": createPublishEntry() },
      }),
      {
        type: "PUBLISH_COMPLETE",
        uploadId: "upload-1",
        newDraftVersionId: "version-42",
      }
    );

    const upload = state.uploads["upload-1"]!;
    expect(upload.uploadType === "publish" && upload.newDraftVersionId).toBe(
      "version-42"
    );
  });

  it("should not modify state for non-existent upload", () => {
    const initial = createState();
    const state = reduce(initial, {
      type: "PUBLISH_COMPLETE",
      uploadId: "non-existent",
      newDraftVersionId: "version-1",
    });

    expect(state).toBe(initial);
  });

  it("should not modify state for non-publish upload", () => {
    const initial = createState({
      uploads: { "upload-1": createYouTubeEntry() },
    });
    const state = reduce(initial, {
      type: "PUBLISH_COMPLETE",
      uploadId: "upload-1",
      newDraftVersionId: "version-1",
    });

    expect(state).toBe(initial);
  });
});

describe("buffer upload lifecycle with stages", () => {
  it("should progress through all buffer stages to success", () => {
    let state = createState();

    state = reduce(state, {
      type: "START_UPLOAD",
      uploadId: "buf-1",
      videoId: "video-1",
      title: "Social Post",
      uploadType: "buffer",
    });
    const started = state.uploads["buf-1"]!;
    expect(started.uploadType === "buffer" && started.bufferStage).toBe(
      "uploading-blob"
    );

    state = reduce(state, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "buf-1",
      stage: "creating-post",
    });
    const creating = state.uploads["buf-1"]!;
    expect(creating.uploadType === "buffer" && creating.bufferStage).toBe(
      "creating-post"
    );

    state = reduce(state, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "buf-1",
      stage: "polling",
    });
    const polling = state.uploads["buf-1"]!;
    expect(polling.uploadType === "buffer" && polling.bufferStage).toBe(
      "polling"
    );

    state = reduce(state, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "buf-1",
      stage: "cleaning-up",
    });
    const cleanup = state.uploads["buf-1"]!;
    expect(cleanup.uploadType === "buffer" && cleanup.bufferStage).toBe(
      "cleaning-up"
    );

    state = reduce(state, {
      type: "UPLOAD_SUCCESS",
      uploadId: "buf-1",
    });
    const success = state.uploads["buf-1"]!;
    expect(success.status).toBe("success");
    expect(success.uploadType === "buffer" && success.bufferStage).toBeNull();
  });

  it("should reset bufferStage to uploading-blob on retry", () => {
    let state = reduce(createState(), {
      type: "START_UPLOAD",
      uploadId: "buf-1",
      videoId: "video-1",
      title: "Retrying Buffer",
      uploadType: "buffer",
    });

    state = reduce(state, {
      type: "UPDATE_BUFFER_STAGE",
      uploadId: "buf-1",
      stage: "polling",
    });
    state = reduce(state, {
      type: "UPLOAD_ERROR",
      uploadId: "buf-1",
      errorMessage: "Poll error",
    });
    state = reduce(state, { type: "RETRY", uploadId: "buf-1" });

    const retried = state.uploads["buf-1"]!;
    expect(retried.status).toBe("uploading");
    expect(retried.uploadType === "buffer" && retried.bufferStage).toBe(
      "uploading-blob"
    );
    expect(retried.progress).toBe(0);
  });
});

describe("export upload lifecycle with stages", () => {
  it("should progress through all export stages to success", () => {
    let state = createState();

    state = reduce(state, {
      type: "START_UPLOAD",
      uploadId: "exp-1",
      videoId: "video-1",
      title: "Export Video",
      uploadType: "export",
    });
    const started = state.uploads["exp-1"]!;
    expect(started.uploadType === "export" && started.exportStage).toBe(
      "queued"
    );

    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "exp-1",
      stage: "concatenating-clips",
    });
    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "exp-1",
      stage: "normalizing-audio",
    });

    state = reduce(state, {
      type: "UPLOAD_SUCCESS",
      uploadId: "exp-1",
    });
    const success = state.uploads["exp-1"]!;
    expect(success.status).toBe("success");
    expect(success.uploadType === "export" && success.exportStage).toBeNull();
    expect(success.progress).toBe(100);
  });

  it("should reset exportStage to queued on retry", () => {
    let state = reduce(createState(), {
      type: "START_UPLOAD",
      uploadId: "exp-1",
      videoId: "video-1",
      title: "Retrying Export",
      uploadType: "export",
    });

    state = reduce(state, {
      type: "UPDATE_EXPORT_STAGE",
      uploadId: "exp-1",
      stage: "normalizing-audio",
    });
    state = reduce(state, {
      type: "UPLOAD_ERROR",
      uploadId: "exp-1",
      errorMessage: "FFmpeg crashed",
    });
    state = reduce(state, { type: "RETRY", uploadId: "exp-1" });

    const retried = state.uploads["exp-1"]!;
    expect(retried.status).toBe("uploading");
    expect(retried.uploadType === "export" && retried.exportStage).toBe(
      "queued"
    );
    expect(retried.progress).toBe(0);
  });
});

describe("publish upload lifecycle with stages", () => {
  it("should progress through publish stages to success", () => {
    let state = createState();

    state = reduce(state, {
      type: "START_UPLOAD",
      uploadId: "pub-1",
      videoId: "",
      title: "My Course",
      uploadType: "publish",
      courseId: "course-1",
    });
    const started = state.uploads["pub-1"]!;
    expect(started.uploadType === "publish" && started.publishStage).toBe(
      "validating"
    );

    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "pub-1",
      stage: "uploading",
    });
    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "pub-1",
      stage: "freezing",
    });
    state = reduce(state, {
      type: "UPDATE_PUBLISH_STAGE",
      uploadId: "pub-1",
      stage: "cloning",
    });
    state = reduce(state, {
      type: "PUBLISH_COMPLETE",
      uploadId: "pub-1",
      newDraftVersionId: "version-2",
    });
    state = reduce(state, {
      type: "UPLOAD_SUCCESS",
      uploadId: "pub-1",
    });

    const success = state.uploads["pub-1"]!;
    expect(success.status).toBe("success");
    if (success.uploadType === "publish") {
      expect(success.newDraftVersionId).toBe("version-2");
    }
  });

  it("should reset publishStage to validating on retry", () => {
    let state = createState({
      uploads: {
        "pub-1": createPublishEntry({
          uploadId: "pub-1",
          publishStage: "freezing",
          status: "retrying",
          retryCount: 1,
        }),
      },
    });

    state = reduce(state, { type: "RETRY", uploadId: "pub-1" });

    const retried = state.uploads["pub-1"]!;
    expect(retried.status).toBe("uploading");
    if (retried.uploadType === "publish") {
      expect(retried.publishStage).toBe("validating");
      expect(retried.newDraftVersionId).toBeNull();
      expect(retried.courseId).toBe("course-1");
    }
  });
});
