/*
 * All the types around the fetch API are broken because of:
 * https://github.com/galaxyproject/galaxy/issues/20227 so this file
 * does a lot of broken type casting to get the tests to run on actual
 * valid data that doesn't match the stated typescript types.
 */
import { describe, expect, it } from "vitest";

import type { AnyFetchTarget, NestedElementItem, UrlDataElement } from "@/api/tools";

import type { ParsedFetchWorkbookColumnType } from "../Collections/wizard/types";
import {
    DerivedColumn,
    type FetchTable,
    fetchTargetToRows,
    fetchTargetToTable,
    type RowType,
    tableToRequest,
} from "./fetchModels";

describe("fetchModels", () => {
    const urlDataElement: UrlDataElement = {
        src: "url",
        name: "file1.txt",
        url: "http://example.com/file1.txt",
        MD5: "md5hash",
        dbkey: "?",
        "SHA-1": "sha1hash",
        "SHA-256": "sha256hash",
        "SHA-512": "sha512hash",
        ext: "txt",
        tags: ["name:file1", "group:treatment:control", "foobar"],
    } as unknown as UrlDataElement;

    function expectRowToHaveColumnValue(
        table: FetchTable,
        rowIndex: number,
        columnType: ParsedFetchWorkbookColumnType,
        expectedValue: any,
    ) {
        const columnKey = new DerivedColumn(columnType, 0, "").key();
        const row = table.rows[rowIndex] as RowType;
        expect(row).toBeDefined();
        expect(row[columnKey]).toEqual(expectedValue);
    }

    const simpleTarget: AnyFetchTarget = {
        destination: { type: "hdas" },
        elements: [urlDataElement],
        auto_decompress: false,
    };

    it("fetchTargetToTable returns correct columns and rows for simple target", () => {
        const table = fetchTargetToTable(simpleTarget);
        const columnTypes = table.columns.map((c) => c.type);
        expect(columnTypes).toEqual(
            expect.arrayContaining([
                "name",
                "url",
                "hash_md5",
                "hash_sha1",
                "hash_sha256",
                "hash_sha512",
                "file_type",
                "dbkey",
                "name_tag",
                "group_tags",
                "tags",
            ]),
        );
        expectRowToHaveColumnValue(table, 0, "name", "file1.txt");
        expectRowToHaveColumnValue(table, 0, "url", "http://example.com/file1.txt");
        expectRowToHaveColumnValue(table, 0, "hash_md5", "md5hash");
        expectRowToHaveColumnValue(table, 0, "hash_sha1", "sha1hash");
        expectRowToHaveColumnValue(table, 0, "hash_sha256", "sha256hash");
        expectRowToHaveColumnValue(table, 0, "hash_sha512", "sha512hash");
        expectRowToHaveColumnValue(table, 0, "file_type", "txt");
        expectRowToHaveColumnValue(table, 0, "dbkey", "?");
        expectRowToHaveColumnValue(table, 0, "name_tag", "file1");
        expectRowToHaveColumnValue(table, 0, "group_tags", "treatment:control");
        expectRowToHaveColumnValue(table, 0, "tags", "foobar");
    });

    it("fetchTargetToRows handles missing optional fields", () => {
        const partialElement: UrlDataElement = {
            src: "url",
            name: "file2.txt",
            url: "http://example.com/file2.txt",
        } as UrlDataElement;
        const columns: DerivedColumn[] = [
            new DerivedColumn("name", 0, "Name"),
            new DerivedColumn("url", 0, "URL"),
            new DerivedColumn("hash_md5", 0, "MD5"),
        ];
        const rows = fetchTargetToRows(
            { collection_type: null, elements: [partialElement] } as AnyFetchTarget,
            columns,
            [],
        );
        expect(rows[0]!["name"]).toEqual("file2.txt");
        expect(rows[0]!["url"]).toEqual("http://example.com/file2.txt");
        expect(rows[0]!["hash_md5"]).toEqual(null);
    });

    it("fetchTargetToTable handles collection_type list", () => {
        const partialElement: UrlDataElement = {
            src: "url",
            name: "A",
            url: "http://example.com/file2.txt",
        } as UrlDataElement;
        const target: AnyFetchTarget = {
            destination: { type: "hdas" },
            collection_type: "list",
            elements: [partialElement],
            auto_decompress: false,
        };
        const table = fetchTargetToTable(target);
        const listIdentifierColKey = "list_identifiers";
        // Should include the identifier value in rows
        const identifierRow = table.rows.find((row) => row[listIdentifierColKey] === "A");
        expect(identifierRow).toBeDefined();
    });

    it("fetchTargetToTable throws on unsupported collection_type", () => {
        const target: AnyFetchTarget = {
            collection_type: "foo",
            elements: [],
            destination: { type: "hdas" },
            auto_decompress: false,
        };
        expect(() => fetchTargetToTable(target)).toThrow(/Unsupported collection type part/);
    });

    it("fetchTargetToRows throws if url is missing", () => {
        const badElement: UrlDataElement = {
            src: "url",
            name: "badfile",
            // url missing
        } as any;
        const columns: DerivedColumn[] = [new DerivedColumn("url", 0, "URL")];
        expect(() =>
            fetchTargetToRows({ collection_type: null, elements: [badElement] } as AnyFetchTarget, columns, []),
        ).toThrow(/URL is required/);
    });

    it("fetchTargetToTable handles nested paired collection", () => {
        const forwardElement: UrlDataElement = {
            src: "url",
            name: "forward",
            url: "http://example.com/sample1-forward1.txt",
        } as UrlDataElement;
        const reverseElement: UrlDataElement = {
            src: "url",
            name: "reverse",
            url: "http://example.com/sample1-reverse1.txt",
        } as UrlDataElement;

        const target: AnyFetchTarget = {
            collection_type: "list:paired",
            elements: [
                {
                    name: "sample1",
                    elements: [forwardElement, reverseElement],
                } as NestedElementItem,
            ],
            destination: { type: "hdca" },
            auto_decompress: false,
        };
        const table = fetchTargetToTable(target);
        // Should include left/right in rows
        expect(table.rows.some((row) => row["paired_identifier"] === "forward")).toBe(true);
        expect(table.rows.some((row) => row["paired_identifier"] === "reverse")).toBe(true);
    });

    describe("tableToRequest", () => {
        const simplestUrlElement: UrlDataElement = {
            src: "url",
            url: "http://example.com/file1.txt",
        } as unknown as UrlDataElement;

        const elementWithTabToSpace: UrlDataElement = {
            src: "url",
            url: "http://example.com/file2.txt",
            space_to_tab: true,
        } as unknown as UrlDataElement;

        const simplestTarget: AnyFetchTarget = {
            destination: { type: "hdas" },
            elements: [simplestUrlElement],
            auto_decompress: false,
        };

        const targetWithDecompressOn: AnyFetchTarget = {
            destination: { type: "hdas" },
            elements: [simplestUrlElement],
            auto_decompress: true,
        };

        const targetWithNoDecompressSpecified: AnyFetchTarget = {
            destination: { type: "hdas" },
            elements: [simplestUrlElement],
        } as unknown as AnyFetchTarget;

        const targetWithTabToSpaceElement: AnyFetchTarget = {
            destination: { type: "hdas" },
            elements: [elementWithTabToSpace],
            auto_decompress: false,
        };

        const targetsForRoundTripCheck: AnyFetchTarget[] = [
            simplestTarget,
            simpleTarget,
            targetWithDecompressOn,
            targetWithTabToSpaceElement,
            targetWithNoDecompressSpecified,
        ];

        it("should round trip any supported target", () => {
            targetsForRoundTripCheck.forEach((target) => {
                // Deep clone target and sort any 'tags' arrays for comparison
                // the order here is not guaranteed in the conversion - at least
                // between different kinds of tags (name, group, etc.)
                function sortTagsDeep(obj: any): any {
                    if (Array.isArray(obj)) {
                        return obj.map(sortTagsDeep);
                    } else if (obj && typeof obj === "object") {
                        const newObj: any = {};
                        for (const key of Object.keys(obj)) {
                            if (key === "tags" && Array.isArray(obj[key])) {
                                newObj[key] = [...obj[key]].sort();
                            } else {
                                newObj[key] = sortTagsDeep(obj[key]);
                            }
                        }
                        return newObj;
                    }
                    return obj;
                }
                const table = fetchTargetToTable(sortTagsDeep(target));
                const restoredTarget = tableToRequest(table);
                expect(sortTagsDeep(restoredTarget)).toEqual(sortTagsDeep(target));
            });
        });
    });
});
