import { renderHook, cleanup } from "@testing-library/react-hooks";
import { type GridCell, GridCellKind, type GridColumn, type Rectangle } from "../src/index.js";
import { getDataEditorTheme, mergeAndRealizeTheme } from "../src/common/styles.js";
import type { DataGridSearchProps } from "../src/internal/data-grid-search/data-grid-search.js";
import { AllCellRenderers } from "../src/cells/index.js";
import type { GetCellRendererCallback } from "../src/cells/cell-types.js";
import type { CellArray, CustomCell } from "../src/internal/data-grid/data-grid-types.js";
import { useColumnSizer } from "../src/data-editor/use-column-sizer.js";
import { vi, expect, describe, it, beforeEach, afterEach } from "vitest";

const COLUMNS: GridColumn[] = [
    {
        title: "A",
        id: "ColumnA",
    },
    {
        title: "B",
        width: 160,
        icon: "headerCode",
    },
];

const A_COPY_OF_COLUMNS_THAT_ALSO_HAS_A_NEW_COLUMN_TITLED_C_THAT_WE_WILL_MEASURE_SOON_ENOUGH: GridColumn[] = [
    ...COLUMNS,
    {
        title: "C",
        id: "ColumnC",
    },
];

const A_BUNCH_OF_COLUMNS_THAT_ALREADY_HAVE_SIZES_WE_DONT_WANT_TO_MEASURE_THESE: GridColumn[] = [
    {
        title: "A",
        width: 120,
    },
    {
        title: "B",
        width: 160,
        icon: "headerCode",
    },
];

type DataBuilder = (x: number, y: number) => string;

function buildCellsForSelectionGetter(dataBuilder: DataBuilder): DataGridSearchProps["getCellsForSelection"] {
    return (selection: Rectangle): CellArray => {
        const result: GridCell[][] = [];

        for (let y = selection.y; y < selection.y + selection.height; y++) {
            const row: GridCell[] = [];
            for (let x = selection.x; x < selection.x + selection.width; x++) {
                const data = dataBuilder(x, y);
                row.push({
                    allowOverlay: false,
                    kind: GridCellKind.Text,
                    displayData: data,
                    data,
                });
            }
            result.push(row);
        }

        return result;
    };
}

const getShortCellsForSelection = vi.fn(buildCellsForSelectionGetter((x, y) => `column ${x} row ${y}`) as any);
const getLongCellsForSelection = vi.fn(
    buildCellsForSelectionGetter((x, y) => `This cell is in column number ${x} and row number ${y}`) as any
);

const theme = getDataEditorTheme();

const abortController = new AbortController();

const SINGLE_COLUMN = [
    {
        title: "A",
        id: "ColumnA",
    },
];

const getCellRenderer: GetCellRendererCallback = cell => {
    if (cell.kind === GridCellKind.Custom) return undefined;
    return AllCellRenderers.find(x => x.kind === cell.kind) as any;
};

describe("use-column-sizer", () => {
    beforeEach(() => {
        getShortCellsForSelection.mockClear();
        getLongCellsForSelection.mockClear();
    });

    afterEach(async () => {
        await cleanup();
    });

    it("Measures a simple cell", async () => {
        const { result } = renderHook(() =>
            useColumnSizer(
                COLUMNS,
                1000,
                getShortCellsForSelection,
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        const columnA = result.current.sizedColumns.find(col => col.title === "A");
        const columnB = result.current.sizedColumns.find(col => col.title === "B");

        expect(columnA).toBeDefined();
        expect(columnB).toBeDefined();

        // Keep in sync with the `displayData` up there.
        expect(columnA?.width).toBe(32);
        expect(columnB?.width).toBe(160);
    });

    //a test to see that it can measure a custom cell
    it("Measures a custom cell", async () => {
        const { result } = renderHook(() =>
            useColumnSizer(
                SINGLE_COLUMN,
                1000,
                (selection: Rectangle): CellArray => {
                    const r: GridCell[][] = [];

                    for (let y = selection.y; y < selection.y + selection.height; y++) {
                        const row: GridCell[] = [];
                        for (let x = selection.x; x < selection.x + selection.width; x++) {
                            const data = `${x} ${y}`;
                            row.push({
                                allowOverlay: false,
                                kind: GridCellKind.Custom,
                                data,
                                copyData: data,
                            });
                        }
                        r.push(row);
                    }

                    return r;
                },
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                () => {
                    return {
                        draw: () => true,
                        kind: GridCellKind.Custom,
                        measure: () => 212,
                        isMatch: (_cell: CustomCell): _cell is CustomCell => true,
                    } as any;
                },
                abortController
            )
        );

        expect(result.current.sizedColumns[0].width).toBe(212);
    });

    it("Measures the last row", async () => {
        // eslint-disable-next-line sonarjs/no-identical-functions
        renderHook(() =>
            useColumnSizer(
                COLUMNS,
                1000,
                getShortCellsForSelection,
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        expect(getShortCellsForSelection).toBeCalledTimes(2);
        expect(getShortCellsForSelection).toHaveBeenNthCalledWith(
            1,
            {
                x: 0,
                y: 0,
                width: 2,
                height: 9,
            },
            expect.anything()
        );
        expect(getShortCellsForSelection).toHaveBeenLastCalledWith(
            {
                x: 0,
                y: 999,
                width: 2,
                height: 1,
            },
            expect.anything()
        );
    });

    it("Measures new columns when they arrive, doesn't re-measure existing ones", async () => {
        const { result, rerender } = renderHook(
            ({ getCellsForSelection, columns }) =>
                useColumnSizer(
                    columns,
                    1000,
                    getCellsForSelection,
                    400,
                    20,
                    500,
                    mergeAndRealizeTheme(theme),
                    getCellRenderer,
                    abortController
                ),
            {
                initialProps: {
                    getCellsForSelection: getShortCellsForSelection,
                    columns: COLUMNS,
                },
            }
        );

        const shortColumnA = result.current.sizedColumns.find(col => col.title === "A");
        const shortColumnB = result.current.sizedColumns.find(col => col.title === "B");
        const shortColumnC = result.current.sizedColumns.find(col => col.title === "C");

        expect(shortColumnA).toBeDefined();
        expect(shortColumnB).toBeDefined();
        expect(shortColumnC).not.toBeDefined();

        // Keep in sync with getShortCellsForSelection up there.
        expect(shortColumnA?.width).toBe(32);
        expect(shortColumnB?.width).toBe(160);

        // Re render with longer text and a new column with title C
        rerender({
            getCellsForSelection: getLongCellsForSelection,
            columns: A_COPY_OF_COLUMNS_THAT_ALSO_HAS_A_NEW_COLUMN_TITLED_C_THAT_WE_WILL_MEASURE_SOON_ENOUGH, // I told you!
        });

        const longColumnA = result.current.sizedColumns.find(col => col.title === "A");
        const longColumnB = result.current.sizedColumns.find(col => col.title === "B");
        const longColumnC = result.current.sizedColumns.find(col => col.title === "C");

        expect(longColumnA).toBeDefined();
        expect(longColumnB).toBeDefined();
        expect(longColumnC).toBeDefined();

        // Keep in sync with getLongCellsForSelection up there.
        expect(longColumnA?.width).toBe(32);
        expect(longColumnB?.width).toBe(160);
        expect(longColumnC?.width).toBe(66);
    });

    it("Returns the default sizes if getCellsForSelection is not provided", async () => {
        const { result } = renderHook(() =>
            useColumnSizer(
                COLUMNS,
                1000,
                undefined,
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        const columnA = result.current.sizedColumns.find(col => col.title === "A");
        const columnB = result.current.sizedColumns.find(col => col.title === "B");

        expect(columnA).toBeDefined();
        expect(columnB).toBeDefined();

        // The default is 150, keep in sync with `defaultSize` in use-cell-sizer.ts
        expect(columnA?.width).toBe(150);
        expect(columnB?.width).toBe(160);
    });

    it("Does not measure anything if every column is sized", async () => {
        const { result } = renderHook(() =>
            useColumnSizer(
                A_BUNCH_OF_COLUMNS_THAT_ALREADY_HAVE_SIZES_WE_DONT_WANT_TO_MEASURE_THESE,
                1000,
                undefined,
                400,
                50,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        expect(result.current.sizedColumns).toBe(
            A_BUNCH_OF_COLUMNS_THAT_ALREADY_HAVE_SIZES_WE_DONT_WANT_TO_MEASURE_THESE
        );
    });

    it("Removes the canvas from the DOM when unmounted", async () => {
        // eslint-disable-next-line sonarjs/no-identical-functions
        const { unmount } = renderHook(() =>
            useColumnSizer(
                COLUMNS,
                1000,
                getShortCellsForSelection,
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        expect(document.querySelector("canvas")).toBeDefined();

        unmount();

        expect(document.querySelector("canvas")).toBeNull();
    });

    it("Distributes extra width among columns if total column width is less than client width", async () => {
        // Setup columns with a known total width smaller than the clientWidth
        const SMALL_COLUMNS: GridColumn[] = [
            {
                title: "A",
                id: "ColumnA",
                width: 100,
            },
            {
                title: "B",
                id: "ColumnB",
                width: 150,
            },
            {
                title: "C",
                id: "ColumnC",
                grow: 1,
            },
        ];

        const { result } = renderHook(() =>
            useColumnSizer(
                SMALL_COLUMNS,
                500,
                getShortCellsForSelection,
                400,
                20,
                500,
                mergeAndRealizeTheme(theme),
                getCellRenderer,
                abortController
            )
        );

        const columnA = result.current.sizedColumns.find(col => col.title === "A");
        const columnB = result.current.sizedColumns.find(col => col.title === "B");
        const columnC = result.current.sizedColumns.find(col => col.title === "C");

        expect(columnA).toBeDefined();
        expect(columnB).toBeDefined();
        expect(columnC).toBeDefined();

        // The original widths
        expect(columnA?.width).toBe(100);
        expect(columnB?.width).toBe(150);
        expect(columnC?.width).toBe(150);
    });
});
