import { http, HttpResponse } from "msw";
import { beforeEach, describe, expect, it, test, vi } from "vitest";

import { useServerMock } from "@/api/client/__mocks__";
import type { CompositeDataElement, HdasUploadTarget, HdcaUploadTarget, NestedElement } from "@/api/tools";

import { createTusUpload } from "./tusUpload";
import {
    type ApiUploadItem,
    buildCollectionUploadPayload,
    buildLegacyPayload,
    buildUploadPayload,
    cleanUrlFilename,
    createFileUploadItem,
    createPastedUploadItem,
    createUrlUploadItem,
    fetchDatasets,
    isGalaxyFileName,
    type LegacyUploadItem,
    parseContentToUploadItems,
    stripGalaxyFilePrefix,
    submitUpload,
    uploadItemDefaults,
} from "./upload";

// Mock the TUS upload functionality
vi.mock("./tusUpload", () => ({
    createTusUpload: vi.fn(),
    NamedBlob: class {},
}));

vi.mock("@/onload/loadConfig", () => ({
    getAppRoot: () => "/",
}));

// Helper to create a mock File object for testing
function createMockFile(name: string, content: string = "test content"): File {
    const blob = new Blob([content], { type: "text/plain" });
    return new File([blob], name, { lastModified: Date.now() });
}

// ============================================================================
// Payload Building Tests
// ============================================================================

describe("buildLegacyPayload", () => {
    test("basic validation", () => {
        expect(() => buildLegacyPayload([], "historyId")).toThrow("No upload items provided.");
        expect(() =>
            buildLegacyPayload([{ fileMode: "new", fileName: "", fileSize: 0 } as LegacyUploadItem], "historyId"),
        ).toThrow("Content not available.");
    });

    test("unknown file mode returns null and triggers validation error", () => {
        // An unknown fileMode causes fromLegacyUploadItem to return null,
        // which then triggers "No valid upload items after conversion" error
        expect(() =>
            buildLegacyPayload(
                [
                    {
                        fileMode: "unknown" as LegacyUploadItem["fileMode"],
                        fileName: "test",
                        fileSize: 1,
                    } as LegacyUploadItem,
                ],
                "historyId",
            ),
        ).toThrow("No valid upload items after conversion.");
    });

    test("empty fileContent validation", () => {
        expect(() =>
            buildLegacyPayload(
                [{ fileMode: "new", fileName: "test", fileContent: "", fileSize: 0 } as LegacyUploadItem],
                "historyId",
            ),
        ).toThrow("Content not available.");

        expect(() =>
            buildLegacyPayload(
                [{ fileMode: "new", fileName: "test", fileContent: "   ", fileSize: 0 } as LegacyUploadItem],
                "historyId",
            ),
        ).toThrow("Content not available.");
    });

    test("invalid URL validation", () => {
        expect(() =>
            buildLegacyPayload(
                [
                    {
                        dbKey: "dbKey",
                        deferred: false,
                        extension: "extension",
                        fileName: "3",
                        fileContent: " http://test.me.0 \n xyz://test.me.1",
                        fileMode: "new",
                        spaceToTab: false,
                        toPosixLines: false,
                        fileSize: 1,
                    } as LegacyUploadItem,
                ],
                "historyId",
            ),
        ).toThrow("Invalid URL: xyz://test.me.1");
    });

    test("pasted content payload", () => {
        const result = buildLegacyPayload(
            [
                {
                    fileContent: " fileContent ",
                    fileMode: "new",
                    fileName: "1",
                    fileSize: 1,
                } as LegacyUploadItem,
            ],
            "historyId",
        );

        expect(result.history_id).toBe("historyId");
        expect(result.auto_decompress).toBe(true);
        expect(result.files).toEqual([]);
        expect(result.targets).toHaveLength(1);

        const target = result.targets[0]!;
        expect(target.destination).toEqual({ type: "hdas" });
        expect(target.elements).toHaveLength(1);

        const element = target.elements![0] as { src: string; paste_content: string };
        expect(element.src).toBe("pasted");
        expect(element.paste_content).toBe(" fileContent ");
    });

    test("local file payload", () => {
        const mockFile = createMockFile("test.txt");
        const result = buildLegacyPayload(
            [
                {
                    dbKey: "hg38",
                    extension: "txt",
                    fileData: mockFile,
                    fileMode: "local",
                    fileName: "test.txt",
                    fileSize: 100,
                    spaceToTab: true,
                    toPosixLines: true,
                } as LegacyUploadItem,
            ],
            "historyId",
        );

        expect(result.files).toHaveLength(1);
        expect(result.files[0]).toBe(mockFile);

        const target = result.targets[0]!;
        const element = target.elements![0] as { src: string; dbkey: string; ext: string };
        expect(element.src).toBe("files");
        expect(element.dbkey).toBe("hg38");
        expect(element.ext).toBe("txt");
    });

    test("URL payload", () => {
        const result = buildLegacyPayload(
            [
                {
                    dbKey: "hg38",
                    deferred: true,
                    extension: "bed",
                    fileName: "remote.bed",
                    fileUri: "http://example.com/data.bed",
                    fileMode: "url",
                    fileSize: 0,
                } as LegacyUploadItem,
            ],
            "historyId",
        );

        expect(result.files).toEqual([]);

        const target = result.targets[0]!;
        const element = target.elements![0] as { src: string; url: string; deferred: boolean };
        expect(element.src).toBe("url");
        expect(element.url).toBe("http://example.com/data.bed");
        expect(element.deferred).toBe(true);
    });

    test("multiple URLs from new mode", () => {
        const result = buildLegacyPayload(
            [
                {
                    fileMode: "new",
                    fileName: "urls",
                    fileContent: "http://example.com/1.txt\nhttp://example.com/2.txt",
                    fileSize: 1,
                } as LegacyUploadItem,
            ],
            "historyId",
        );

        const target = result.targets[0]!;
        expect(target.elements).toHaveLength(2);

        const elem1 = target.elements![0] as { src: string; url: string };
        const elem2 = target.elements![1] as { src: string; url: string };
        expect(elem1.src).toBe("url");
        expect(elem1.url).toBe("http://example.com/1.txt");
        expect(elem2.url).toBe("http://example.com/2.txt");
    });

    test("Galaxy filename stripping", () => {
        const mockFile = createMockFile("Galaxy5-[PreviousFile].bed");
        const result = buildLegacyPayload(
            [
                {
                    fileData: mockFile,
                    fileMode: "local",
                    fileName: "Galaxy5-[PreviousFile].bed",
                    fileSize: 100,
                } as LegacyUploadItem,
            ],
            "historyId",
        );

        const target = result.targets[0]!;
        const element = target.elements![0] as { name: string };
        expect(element.name).toBe("PreviousFile");
    });

    test("composite payload", () => {
        const mockFile1 = createMockFile("file1.txt");
        const mockFile2 = createMockFile("file2.txt");

        const result = buildLegacyPayload(
            [
                {
                    fileContent: "fileContent",
                    fileMode: "new",
                    fileName: "1",
                    fileSize: 1,
                } as LegacyUploadItem,
                {
                    dbKey: "hg38",
                    extension: "txt",
                    fileData: mockFile1,
                    fileMode: "local",
                    fileName: "2",
                    fileSize: 100,
                } as LegacyUploadItem,
                {
                    dbKey: "hg38",
                    extension: "bed",
                    fileData: mockFile2,
                    fileMode: "local",
                    fileName: "Galaxy2-[PreviousFile].bed",
                    fileSize: 100,
                } as LegacyUploadItem,
            ],
            "historyId",
            true,
        );

        expect(result.files).toHaveLength(2);

        const target = result.targets[0] as HdasUploadTarget;
        // For composite uploads, elements are wrapped in a composite structure
        expect(target.elements).toBeDefined();
        expect(target.elements).toHaveLength(1);

        const compositeElement = target.elements![0]! as CompositeDataElement;
        expect(compositeElement.src).toBe("composite");
        // 3 elements: 1 pasted + 2 local files
        expect(compositeElement.composite.elements).toHaveLength(3);
    });
});

describe("isGalaxyFileName", () => {
    test("recognizes Galaxy file names", () => {
        expect(isGalaxyFileName("Galaxy5-[MyFile].bed")).toBe(true);
        expect(isGalaxyFileName("Galaxy123-[Some File Name].txt")).toBe(true);
    });

    test("rejects non-Galaxy file names", () => {
        expect(isGalaxyFileName("myfile.txt")).toBe(false);
        expect(isGalaxyFileName("Galaxy-[NoNumber].txt")).toBe(false);
        expect(isGalaxyFileName(null)).toBe(false);
        expect(isGalaxyFileName(undefined)).toBe(false);
    });
});

describe("stripGalaxyFilePrefix", () => {
    test("strips Galaxy prefix", () => {
        expect(stripGalaxyFilePrefix("Galaxy5-[MyFile].bed")).toBe("MyFile");
        expect(stripGalaxyFilePrefix("Galaxy123-[Some File].txt")).toBe("Some File");
    });

    test("returns original if no match", () => {
        expect(stripGalaxyFilePrefix("myfile.txt")).toBe("myfile.txt");
    });
});

describe("cleanUrlFilename", () => {
    test("removes URL path and query parameters", () => {
        expect(cleanUrlFilename("http://example.com/path/to/file.pdf?download=1")).toBe("file.pdf");
    });

    test("decodes URL-encoded characters", () => {
        expect(cleanUrlFilename("file%20name.pdf")).toBe("file name.pdf");
        expect(cleanUrlFilename("Readme%20Statistical%20Downscaling.pdf")).toBe("Readme Statistical Downscaling.pdf");
        expect(cleanUrlFilename("special%26chars%3D.txt")).toBe("special&chars=.txt");
    });

    test("handles both encoding and query parameters", () => {
        expect(cleanUrlFilename("Readme%20Statistical%20Downscaling.pdf?download=1")).toBe(
            "Readme Statistical Downscaling.pdf",
        );
        expect(cleanUrlFilename("my%20file.txt?token=abc123")).toBe("my file.txt");
    });

    test("returns original for normal filenames", () => {
        expect(cleanUrlFilename("normal.txt")).toBe("normal.txt");
        expect(cleanUrlFilename("file-with-dashes.bed")).toBe("file-with-dashes.bed");
    });

    test("handles malformed URL encoding gracefully", () => {
        // Malformed percent encoding (invalid hex)
        expect(cleanUrlFilename("file%ZZname.txt")).toBe("file%ZZname.txt");
        // Incomplete percent encoding
        expect(cleanUrlFilename("file%2.txt")).toBe("file%2.txt");
    });

    test("handles empty query string", () => {
        expect(cleanUrlFilename("file.txt?")).toBe("file.txt");
    });

    test("returns null for empty filename", () => {
        expect(cleanUrlFilename("")).toBeNull();
    });

    test("returns null for URL with no filename", () => {
        expect(cleanUrlFilename("http://example.com/")).toBeNull();
    });
});

describe("createFileUploadItem", () => {
    test("creates file upload item with defaults", () => {
        const mockFile = createMockFile("test.txt", "content");
        const item = createFileUploadItem(mockFile, "historyId");

        expect(item.src).toBe("files");
        expect(item.fileData).toBe(mockFile);
        expect(item.historyId).toBe("historyId");
        expect(item.name).toBe("test.txt");
        expect(item.size).toBe(mockFile.size);
        expect(item.dbkey).toBe(uploadItemDefaults.dbkey);
        expect(item.ext).toBe(uploadItemDefaults.ext);
        expect(item.space_to_tab).toBe(uploadItemDefaults.space_to_tab);
        expect(item.to_posix_lines).toBe(uploadItemDefaults.to_posix_lines);
        expect(item.deferred).toBe(uploadItemDefaults.deferred);
    });

    test("creates file upload item with custom options", () => {
        const mockFile = createMockFile("test.txt");
        const item = createFileUploadItem(mockFile, "historyId", {
            name: "custom.bed",
            dbkey: "hg38",
            ext: "bed",
            space_to_tab: true,
            to_posix_lines: false,
            deferred: true,
        });

        expect(item.name).toBe("custom.bed");
        expect(item.dbkey).toBe("hg38");
        expect(item.ext).toBe("bed");
        expect(item.space_to_tab).toBe(true);
        expect(item.to_posix_lines).toBe(false);
        expect(item.deferred).toBe(true);
    });
});

describe("createPastedUploadItem", () => {
    test("creates pasted upload item with defaults", () => {
        const item = createPastedUploadItem("pasted content", "historyId");

        expect(item.src).toBe("pasted");
        expect(item.paste_content).toBe("pasted content");
        expect(item.historyId).toBe("historyId");
        expect(item.name).toBe("New File");
        expect(item.size).toBe("pasted content".length);
        expect(item.dbkey).toBe(uploadItemDefaults.dbkey);
        expect(item.ext).toBe(uploadItemDefaults.ext);
    });

    test("creates pasted upload item with custom name", () => {
        const item = createPastedUploadItem("content", "historyId", { name: "MyPaste.txt" });
        expect(item.name).toBe("MyPaste.txt");
    });
});

describe("createUrlUploadItem", () => {
    test("creates URL upload item with defaults", () => {
        const item = createUrlUploadItem("http://example.com/data.bed", "historyId");

        expect(item.src).toBe("url");
        expect(item.url).toBe("http://example.com/data.bed");
        expect(item.historyId).toBe("historyId");
        expect(item.name).toBe("data.bed"); // extracted from URL
        expect(item.size).toBe(0);
        expect(item.dbkey).toBe(uploadItemDefaults.dbkey);
        expect(item.deferred).toBe(uploadItemDefaults.deferred);
    });

    test("extracts filename from URL path", () => {
        const item = createUrlUploadItem("http://example.com/path/to/file.txt?query=param", "historyId");
        expect(item.name).toBe("file.txt");
    });

    test("creates URL upload item with custom options", () => {
        const item = createUrlUploadItem("http://example.com/data.bed", "historyId", {
            name: "custom-name.bed",
            deferred: true,
            dbkey: "hg38",
        });

        expect(item.name).toBe("custom-name.bed");
        expect(item.deferred).toBe(true);
        expect(item.dbkey).toBe("hg38");
    });
});

describe("parseContentToUploadItems", () => {
    test("throws on empty content", () => {
        expect(() => parseContentToUploadItems("", "historyId")).toThrow("Content not available.");
        expect(() => parseContentToUploadItems("   ", "historyId")).toThrow("Content not available.");
    });

    test("parses plain text as pasted content", () => {
        const items = parseContentToUploadItems("some plain text\nwith multiple lines", "historyId");

        expect(items).toHaveLength(1);
        expect(items[0]!.src).toBe("pasted");
        expect((items[0] as { paste_content: string }).paste_content).toBe("some plain text\nwith multiple lines");
    });

    test("parses URLs when first line is a URL", () => {
        const items = parseContentToUploadItems(
            "http://example.com/file1.txt\nhttp://example.com/file2.txt",
            "historyId",
        );

        expect(items).toHaveLength(2);
        expect(items[0]!.src).toBe("url");
        expect((items[0] as { url: string }).url).toBe("http://example.com/file1.txt");
        expect(items[1]!.src).toBe("url");
        expect((items[1] as { url: string }).url).toBe("http://example.com/file2.txt");
    });

    test("throws on invalid URL in URL list", () => {
        expect(() => parseContentToUploadItems("http://example.com/valid.txt\ninvalid-not-a-url", "historyId")).toThrow(
            "Invalid URL: invalid-not-a-url",
        );
    });

    test("handles whitespace around URLs", () => {
        const items = parseContentToUploadItems("  http://example.com/file.txt  \n  ", "historyId");

        expect(items).toHaveLength(1);
        expect((items[0] as { url: string }).url).toBe("http://example.com/file.txt");
    });
});

describe("buildUploadPayload", () => {
    test("throws on empty items", () => {
        expect(() => buildUploadPayload([])).toThrow("No upload items provided.");
    });

    test("throws on mixed history IDs", () => {
        const file1 = createMockFile("file1.txt");
        const file2 = createMockFile("file2.txt");
        const items: ApiUploadItem[] = [
            createFileUploadItem(file1, "history1"),
            createFileUploadItem(file2, "history2"),
        ];

        expect(() => buildUploadPayload(items)).toThrow("All upload items must target the same history.");
    });

    test("builds payload with file upload items", () => {
        const mockFile = createMockFile("test.txt");
        const items: ApiUploadItem[] = [createFileUploadItem(mockFile, "historyId")];

        const result = buildUploadPayload(items);

        expect(result.history_id).toBe("historyId");
        expect(result.auto_decompress).toBe(true);
        expect(result.files).toHaveLength(1);
        expect(result.files[0]).toBe(mockFile);
        expect(result.targets).toHaveLength(1);
        expect(result.targets[0]!.destination).toEqual({ type: "hdas" });
        expect(result.targets[0]!.elements).toHaveLength(1);
    });

    test("builds composite payload", () => {
        const file1 = createMockFile("file1.txt");
        const file2 = createMockFile("file2.txt");
        const items: ApiUploadItem[] = [
            createFileUploadItem(file1, "historyId"),
            createFileUploadItem(file2, "historyId"),
        ];

        const result = buildUploadPayload(items, { composite: true });

        expect(result.files).toHaveLength(2);
        expect(result.targets[0]!.elements).toHaveLength(1);

        const compositeElement = result.targets[0]!.elements![0] as CompositeDataElement;
        expect(compositeElement.src).toBe("composite");
        expect(compositeElement.composite.elements).toHaveLength(2);
    });

    test("validates empty file data", () => {
        const emptyFile = new File([], "empty.txt");
        const items: ApiUploadItem[] = [createFileUploadItem(emptyFile, "historyId")];

        expect(() => buildUploadPayload(items)).toThrow("File data is empty for upload item: empty.txt");
    });

    test("validates empty pasted content", () => {
        const items: ApiUploadItem[] = [createPastedUploadItem("", "historyId", { name: "empty" })];

        expect(() => buildUploadPayload(items)).toThrow("No content for pasted upload item: empty");
    });

    test("validates invalid URL", () => {
        const items: ApiUploadItem[] = [createUrlUploadItem("not-a-valid-url", "historyId")];

        expect(() => buildUploadPayload(items)).toThrow("Invalid URL: not-a-valid-url");
    });
});

// ============================================================================
// Collection Upload Payload Building Tests
// ============================================================================

describe("buildCollectionUploadPayload", () => {
    test("throws on empty items", () => {
        expect(() => buildCollectionUploadPayload([], { collectionName: "test", collectionType: "list" })).toThrow(
            "No upload items provided.",
        );
    });

    test("throws on mixed history IDs", () => {
        const items: ApiUploadItem[] = [
            createUrlUploadItem("http://example.com/1.txt", "history1"),
            createUrlUploadItem("http://example.com/2.txt", "history2"),
        ];

        expect(() => buildCollectionUploadPayload(items, { collectionName: "test", collectionType: "list" })).toThrow(
            "All upload items must target the same history.",
        );
    });

    test("builds list collection payload with URL items", () => {
        const items: ApiUploadItem[] = [
            createUrlUploadItem("http://example.com/1.txt", "historyId"),
            createUrlUploadItem("http://example.com/2.txt", "historyId"),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "My List",
            collectionType: "list",
        });

        expect(result.history_id).toBe("historyId");
        expect(result.auto_decompress).toBe(true);
        expect(result.files).toEqual([]);
        expect(result.targets).toHaveLength(1);

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.destination).toEqual({ type: "hdca" });
        expect(target.collection_type).toBe("list");
        expect(target.name).toBe("My List");
        expect(target.elements).toHaveLength(2);

        const elem1 = target.elements[0] as { src: string; url: string };
        const elem2 = target.elements[1] as { src: string; url: string };
        expect(elem1.src).toBe("url");
        expect(elem1.url).toBe("http://example.com/1.txt");
        expect(elem2.src).toBe("url");
        expect(elem2.url).toBe("http://example.com/2.txt");
    });

    test("builds list collection payload with local file items", () => {
        const file1 = createMockFile("file1.txt");
        const file2 = createMockFile("file2.txt");
        const items: ApiUploadItem[] = [
            createFileUploadItem(file1, "historyId"),
            createFileUploadItem(file2, "historyId"),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "File List",
            collectionType: "list",
        });

        expect(result.files).toHaveLength(2);
        expect(result.files[0]).toBe(file1);
        expect(result.files[1]).toBe(file2);

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.destination).toEqual({ type: "hdca" });
        expect(target.collection_type).toBe("list");
        expect(target.elements).toHaveLength(2);

        const elem1 = target.elements[0] as { src: string };
        const elem2 = target.elements[1] as { src: string };
        expect(elem1.src).toBe("files");
        expect(elem2.src).toBe("files");
    });

    test("builds list:paired collection payload with nested elements", () => {
        const items: ApiUploadItem[] = [
            createUrlUploadItem("http://example.com/sample_R1.fastq", "historyId", { name: "sample_R1.fastq" }),
            createUrlUploadItem("http://example.com/sample_R2.fastq", "historyId", { name: "sample_R2.fastq" }),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "Paired List",
            collectionType: "list:paired",
        });

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.destination).toEqual({ type: "hdca" });
        expect(target.collection_type).toBe("list:paired");
        expect(target.name).toBe("Paired List");
        expect(target.elements).toHaveLength(1); // 1 pair

        const pair = target.elements[0] as NestedElement;
        expect(pair.name).toBe("sample");
        expect(pair.elements).toHaveLength(2);

        const fwd = pair.elements[0] as { name: string; src: string; url: string };
        const rev = pair.elements[1] as { name: string; src: string; url: string };
        expect(fwd.name).toBe("forward");
        expect(fwd.src).toBe("url");
        expect(rev.name).toBe("reverse");
        expect(rev.src).toBe("url");
    });

    test("builds list:paired with multiple pairs", () => {
        const items: ApiUploadItem[] = [
            createUrlUploadItem("http://example.com/s1_R1.fastq", "historyId", { name: "s1_R1.fastq" }),
            createUrlUploadItem("http://example.com/s1_R2.fastq", "historyId", { name: "s1_R2.fastq" }),
            createUrlUploadItem("http://example.com/s2_R1.fastq", "historyId", { name: "s2_R1.fastq" }),
            createUrlUploadItem("http://example.com/s2_R2.fastq", "historyId", { name: "s2_R2.fastq" }),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "Two Pairs",
            collectionType: "list:paired",
        });

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.elements).toHaveLength(2); // 2 pairs

        const pair1 = target.elements[0] as NestedElement;
        const pair2 = target.elements[1] as NestedElement;
        expect(pair1.name).toBe("s1");
        expect(pair2.name).toBe("s2");
    });

    test("files array order matches depth-first element traversal for list:paired", () => {
        const file1 = createMockFile("s1_R1.fastq");
        const file2 = createMockFile("s1_R2.fastq");
        const file3 = createMockFile("s2_R1.fastq");
        const file4 = createMockFile("s2_R2.fastq");

        const items: ApiUploadItem[] = [
            createFileUploadItem(file1, "historyId", { name: "s1_R1.fastq" }),
            createFileUploadItem(file2, "historyId", { name: "s1_R2.fastq" }),
            createFileUploadItem(file3, "historyId", { name: "s2_R1.fastq" }),
            createFileUploadItem(file4, "historyId", { name: "s2_R2.fastq" }),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "Paired Files",
            collectionType: "list:paired",
        });

        // Files must be in order: pair1-fwd, pair1-rev, pair2-fwd, pair2-rev
        // This matches the backend's depth-first replace_file_srcs iteration
        expect(result.files).toHaveLength(4);
        expect(result.files[0]).toBe(file1);
        expect(result.files[1]).toBe(file2);
        expect(result.files[2]).toBe(file3);
        expect(result.files[3]).toBe(file4);
    });

    test("handles mixed element types (files + URLs)", () => {
        const file1 = createMockFile("local.txt");
        const items: ApiUploadItem[] = [
            createFileUploadItem(file1, "historyId"),
            createUrlUploadItem("http://example.com/remote.txt", "historyId"),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "Mixed List",
            collectionType: "list",
        });

        expect(result.files).toHaveLength(1);
        expect(result.files[0]).toBe(file1);

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.elements).toHaveLength(2);

        const elem1 = target.elements[0] as { src: string };
        const elem2 = target.elements[1] as { src: string };
        expect(elem1.src).toBe("files");
        expect(elem2.src).toBe("url");
    });

    test("handles pasted content in collection", () => {
        const items: ApiUploadItem[] = [
            createPastedUploadItem("content 1", "historyId", { name: "paste1.txt" }),
            createPastedUploadItem("content 2", "historyId", { name: "paste2.txt" }),
        ];

        const result = buildCollectionUploadPayload(items, {
            collectionName: "Pasted List",
            collectionType: "list",
        });

        expect(result.files).toEqual([]);

        const target = result.targets[0] as HdcaUploadTarget;
        expect(target.elements).toHaveLength(2);

        const elem1 = target.elements[0] as { src: string; paste_content: string };
        expect(elem1.src).toBe("pasted");
        expect(elem1.paste_content).toBe("content 1");
    });

    test("validates empty file data", () => {
        const emptyFile = new File([], "empty.txt");
        const items: ApiUploadItem[] = [createFileUploadItem(emptyFile, "historyId")];

        expect(() => buildCollectionUploadPayload(items, { collectionName: "test", collectionType: "list" })).toThrow(
            "File data is empty for upload item: empty.txt",
        );
    });

    test("validates invalid URL", () => {
        const items: ApiUploadItem[] = [createUrlUploadItem("not-a-url", "historyId")];

        expect(() => buildCollectionUploadPayload(items, { collectionName: "test", collectionType: "list" })).toThrow(
            "Invalid URL: not-a-url",
        );
    });
});

// ============================================================================
// Upload Submission Tests
// ============================================================================

describe("upload submission", () => {
    const { server } = useServerMock();

    beforeEach(() => {
        vi.clearAllMocks();
    });

    describe("fetchDatasets", () => {
        it("should successfully send payload to API", async () => {
            const mockResponse = { jobs: [{ id: "job123" }], outputs: [{ id: "dataset1" }] };
            const successCallback = vi.fn();

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json(mockResponse);
                }),
            );

            await fetchDatasets(
                {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                },
                { success: successCallback },
            );

            expect(successCallback).toHaveBeenCalledWith(mockResponse);
        });

        it("should handle API errors", async () => {
            const errorCallback = vi.fn();

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ err_msg: "Upload failed" }, { status: 500 });
                }),
            );

            await fetchDatasets(
                {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                },
                { error: errorCallback },
            );

            expect(errorCallback).toHaveBeenCalled();
            const errorArg = errorCallback.mock.calls[0]?.[0];
            expect(errorArg).toBe("Upload failed");
        });
    });

    describe("submitUpload", () => {
        it("should immediately fail if error_message is present", async () => {
            const errorCallback = vi.fn();

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    error_message: "Validation failed",
                    files: [],
                },
                error: errorCallback,
            });

            expect(errorCallback).toHaveBeenCalledWith("Validation failed");
            expect(createTusUpload).not.toHaveBeenCalled();
        });

        it("should upload files via TUS when files are present", async () => {
            const mockFile = new File(["content"], "test.txt");
            const successCallback = vi.fn();
            const progressCallback = vi.fn();

            vi.mocked(createTusUpload).mockResolvedValue({
                sessionId: "session123",
                fileName: "test.txt",
            });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job789" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [
                        {
                            destination: { type: "hdas" },
                            elements: [
                                {
                                    src: "files",
                                    name: "test.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                            ],
                            auto_decompress: true,
                        },
                    ],
                    auto_decompress: true,
                    files: [mockFile],
                },
                success: successCallback,
                progress: progressCallback,
            });

            expect(createTusUpload).toHaveBeenCalledWith({
                file: mockFile,
                endpoint: "/api/upload/resumable_upload/",
                historyId: "hist123",
                chunkSize: 10485760,
                onProgress: expect.any(Function),
                onError: expect.any(Function),
            });

            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job789" }] });
        });

        it("should upload via TUS for composite uploads even without files", async () => {
            const successCallback = vi.fn();

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_composite" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    files: [],
                },
                success: successCallback,
                isComposite: true,
            });

            // For composite with no files, should still go through TUS path
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_composite" }] });
        });

        it("should directly submit URL uploads without TUS", async () => {
            const successCallback = vi.fn();

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_url" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [
                        {
                            destination: { type: "hdas" },
                            elements: [
                                {
                                    src: "url",
                                    url: "https://example.com/file.txt",
                                    name: "file.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                            ],
                            auto_decompress: true,
                        },
                    ],
                    auto_decompress: true,
                    files: [],
                },
                success: successCallback,
            });

            expect(createTusUpload).not.toHaveBeenCalled();
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_url" }] });
        });

        it("should convert pasted content to blob and upload via TUS", async () => {
            const successCallback = vi.fn();

            vi.mocked(createTusUpload).mockResolvedValue({
                sessionId: "session_paste",
                fileName: "pasted.txt",
            });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_paste" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [
                        {
                            destination: { type: "hdas" },
                            elements: [
                                {
                                    src: "pasted",
                                    paste_content: "Hello, world!",
                                    name: "pasted.txt",
                                    dbkey: "?",
                                    ext: "txt",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                            ],
                            auto_decompress: true,
                        },
                    ],
                    auto_decompress: true,
                    files: [],
                },
                success: successCallback,
            });

            expect(createTusUpload).toHaveBeenCalled();
            const tusCall = vi.mocked(createTusUpload).mock.calls[0];
            expect(tusCall?.[0].file).toBeInstanceOf(Blob);
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_paste" }] });
        });

        it("should use custom chunk size if provided", async () => {
            const mockFile = new File(["content"], "chunked.txt");
            const customChunkSize = 5242880; // 5MB

            vi.mocked(createTusUpload).mockResolvedValue({
                sessionId: "session_chunk",
                fileName: "chunked.txt",
            });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_chunk" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    files: [mockFile],
                },
                chunkSize: customChunkSize,
            });

            expect(createTusUpload).toHaveBeenCalledWith(
                expect.objectContaining({
                    chunkSize: customChunkSize,
                }),
            );
        });

        it("should handle multiple file uploads sequentially", async () => {
            const file1 = new File(["content1"], "file1.txt");
            const file2 = new File(["content2"], "file2.txt");
            const successCallback = vi.fn();

            vi.mocked(createTusUpload)
                .mockResolvedValueOnce({
                    sessionId: "session1",
                    fileName: "file1.txt",
                })
                .mockResolvedValueOnce({
                    sessionId: "session2",
                    fileName: "file2.txt",
                });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_multi" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    files: [file1, file2],
                },
                success: successCallback,
            });

            expect(createTusUpload).toHaveBeenCalledTimes(2);
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_multi" }] });
        });

        it("should invoke progress callback during upload", async () => {
            const mockFile = new File(["content"], "progress.txt");
            const progressCallback = vi.fn();

            vi.mocked(createTusUpload).mockImplementation(async (options) => {
                // Simulate progress updates
                options.onProgress(25);
                options.onProgress(50);
                options.onProgress(100);
                return {
                    sessionId: "session_progress",
                    fileName: "progress.txt",
                };
            });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_progress" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    files: [mockFile],
                },
                progress: progressCallback,
            });

            expect(progressCallback).toHaveBeenCalledWith(25);
            expect(progressCallback).toHaveBeenCalledWith(50);
            expect(progressCallback).toHaveBeenCalledWith(100);
        });

        it("should handle TUS upload errors", async () => {
            const mockFile = new File(["content"], "error.txt");
            const errorCallback = vi.fn();

            vi.mocked(createTusUpload).mockRejectedValue(new Error("Upload failed"));

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [],
                    auto_decompress: true,
                    files: [mockFile],
                },
                error: errorCallback,
            });

            expect(errorCallback).toHaveBeenCalled();
        });

        it("should directly submit HDCA collection target with URLs (no TUS)", async () => {
            const successCallback = vi.fn();

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_hdca" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [
                        {
                            destination: { type: "hdca" },
                            collection_type: "list",
                            name: "My Collection",
                            auto_decompress: false,
                            elements: [
                                {
                                    src: "url",
                                    url: "https://example.com/1.txt",
                                    name: "1.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                                {
                                    src: "url",
                                    url: "https://example.com/2.txt",
                                    name: "2.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                            ],
                        },
                    ],
                    auto_decompress: true,
                    files: [],
                },
                success: successCallback,
            });

            expect(createTusUpload).not.toHaveBeenCalled();
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_hdca" }] });
        });

        it("should upload HDCA collection with local files via TUS", async () => {
            const file1 = new File(["content1"], "file1.txt");
            const file2 = new File(["content2"], "file2.txt");
            const successCallback = vi.fn();

            vi.mocked(createTusUpload)
                .mockResolvedValueOnce({
                    sessionId: "session1",
                    fileName: "file1.txt",
                })
                .mockResolvedValueOnce({
                    sessionId: "session2",
                    fileName: "file2.txt",
                });

            server.use(
                http.post("/api/tools/fetch", () => {
                    return HttpResponse.json({ jobs: [{ id: "job_hdca_files" }] });
                }),
            );

            await submitUpload({
                data: {
                    history_id: "hist123",
                    targets: [
                        {
                            destination: { type: "hdca" },
                            collection_type: "list",
                            name: "File Collection",
                            auto_decompress: false,
                            elements: [
                                {
                                    src: "files",
                                    name: "file1.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                                {
                                    src: "files",
                                    name: "file2.txt",
                                    dbkey: "?",
                                    ext: "auto",
                                    space_to_tab: false,
                                    to_posix_lines: false,
                                    auto_decompress: false,
                                    deferred: false,
                                },
                            ],
                        },
                    ],
                    auto_decompress: true,
                    files: [file1, file2],
                },
                success: successCallback,
            });

            expect(createTusUpload).toHaveBeenCalledTimes(2);
            expect(successCallback).toHaveBeenCalledWith({ jobs: [{ id: "job_hdca_files" }] });
        });
    });
});
