// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { PageView, ColumnSelector, test, expect } from "../helpers.ts";

test.describe.configure({ mode: "parallel" });

type SidebarState =
    | {
          open: true;
          tabs: string[];
          selectedTab: string;
          type: string | "expression";
          snapshot: string;
      }
    | { open: false };

type Action =
    | "open"
    | "activate"
    | "deactivate"
    | "reorder"
    | "aggregate"
    | "deaggregate"
    | "groupby"
    | "splitby"
    | "orderby"
    | "where";

// Given an initial state and a list of possible actions, expect the given outputs.
// Each action will be tested against the three methods - button click,
// drag'n'drop, and text input - according to what is possible.
// For instance, it's impossible to button click a column into a query position,
// so this will be skipped.
type TestSpec = {
    initial_state: { open: boolean; active: boolean; columns?: string[] };
    actions: Action[];
    outputs: {
        table_col?: ExpectedState;
        expr_col?: ExpectedState;
    };
};
type ExpectedState =
    | { closed: true }
    | { unchanged: true }
    | ({ tabs: string[]; selectedTab: string } & { type?: string });

// Note: impossible states are not encoded here.
const TEST_SPEC: Record<string, TestSpec> = {
    "Open - Active": {
        initial_state: {
            open: false,
            active: true,
        },
        actions: ["open"],
        outputs: {
            table_col: {
                tabs: ["Style"],
                selectedTab: "Style",
            },
            expr_col: {
                tabs: ["Style", "Attributes"],
                selectedTab: "Style",
            },
        },
    },
    "Open - Inactive": {
        initial_state: {
            open: false,
            active: false,
        },
        actions: ["open"],
        outputs: {
            // The Style tab requires the column to be in
            // `view_config.columns` (see `column_settings_sidebar.rs`
            // `show_styles`); inactive expression columns only see
            // Attributes.
            expr_col: {
                tabs: ["Attributes"],
                selectedTab: "Attributes",
            },
        },
    },
    Deactivate: {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["deactivate"],
        outputs: {
            table_col: {
                closed: true,
            },
            // Deactivating drops the column from `view_config.columns`,
            // which removes the Style tab; the sidebar stays open on
            // the expression with only Attributes left.
            expr_col: {
                tabs: ["Attributes"],
                selectedTab: "Attributes",
            },
        },
    },
    Activate: {
        initial_state: {
            open: true,
            active: false,
        },
        actions: ["activate"],
        outputs: {
            expr_col: {
                tabs: ["Style", "Attributes"],
                selectedTab: "Attributes",
            },
        },
    },
    "Reorder - Active": {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["reorder"],
        outputs: {
            table_col: {
                unchanged: true,
            },
            expr_col: {
                unchanged: true,
            },
        },
    },
    Aggregate: {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["aggregate"],
        outputs: {
            table_col: {
                unchanged: true,
                type: "integer",
            },
            expr_col: {
                unchanged: true,
                type: "integer",
            },
        },
    },
    Deaggregate: {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["deaggregate"],
        outputs: {
            table_col: {
                unchanged: true,
                type: "string",
            },
            expr_col: {
                unchanged: true,
                type: "string",
            },
        },
    },
    "Groupby - Active": {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["groupby"],
        outputs: {
            expr_col: {
                unchanged: true,
                type: "integer",
            },
            table_col: {
                unchanged: true,
                type: "integer",
            },
        },
    },
    "Splitby - Active": {
        initial_state: {
            open: true,
            active: true,
        },
        actions: ["splitby"],
        outputs: {
            expr_col: {
                unchanged: true,
            },
            table_col: {
                unchanged: true,
            },
        },
    },
    "Filter - Active": {
        initial_state: { open: true, active: true },
        actions: ["orderby", "where"],
        outputs: {
            expr_col: { unchanged: true },
            table_col: { unchanged: true },
        },
    },
    "Query - Inactive": {
        initial_state: {
            open: true,
            active: false,
        },
        actions: ["groupby", "splitby", "orderby", "where"],
        outputs: {
            expr_col: {
                unchanged: true,
            },
        },
    },
};

/** Restores the viewer to a simple state and returns the columnSettingsSidebar's innerHTML */
async function open(
    view: PageView,
    initial_state: TestSpec["initial_state"],
    expr: boolean,
): Promise<[ColumnSelector, SidebarState]> {
    const { active, open, columns } = initial_state;

    // expr_value is a string to keep things consistent between aggregate values
    const expr_name = "expr";
    const expr_value = "'hello'";

    let view_columns = columns ? [...columns] : ["Category", "City"];
    if (active && expr) {
        view_columns.push(expr_name);
    }

    const viewer_settings = {
        settings: true,
        columns: view_columns,
        expressions: { [expr_name]: expr_value },
    };

    await view.restore(viewer_settings);

    await view.page.evaluate((viewer_settings) => {
        console.log(viewer_settings);
    }, viewer_settings);

    let selector = active
        ? await view.settingsPanel.activeColumns.getColumnByName(
              expr ? expr_name : view_columns[0],
          )
        : expr
          ? await view.settingsPanel.inactiveColumns.getColumnByName(expr_name)
          : view.settingsPanel.inactiveColumns.getColumnByType("string");

    let state: SidebarState;
    if (open) {
        await selector.editBtn.click();
        state = {
            open: true,
            tabs: await view.columnSettingsSidebar.getTabs(),
            selectedTab: await view.columnSettingsSidebar.getSelectedTab(),
            type: await view.columnSettingsSidebar.getType(),
            snapshot: await view.columnSettingsSidebar.container.innerHTML(),
        };
    } else {
        state = { open: false };
    }
    return [selector, state];
}

async function checkOutput(
    view: PageView,
    expected: ExpectedState,
    initial_state: SidebarState,
) {
    const POSSIBLE_TABS = ["Style", "Attributes"];

    const check_tabs = async (
        selectedTab: string,
        tabs: string[],
        unexpected_tabs: string[],
    ) => {
        if (selectedTab === "") {
            await view.columnSettingsSidebar.container
                .locator(".settings_tab.selected_tab .tab-title")
                .first()
                .waitFor();
        } else {
            await view.columnSettingsSidebar.container
                .locator(`.settings_tab.selected_tab #${selectedTab}`)
                .waitFor();
        }

        for (let tab of tabs) {
            if (tab === "") {
                await view.columnSettingsSidebar.container
                    .locator(`.tab-title`)
                    .first()
                    .waitFor();
            } else {
                await view.columnSettingsSidebar.container
                    .locator(`#${tab}`)
                    .waitFor();
            }
        }
        for (let tab of unexpected_tabs) {
            await view.columnSettingsSidebar.tabTitle
                .locator(`#${tab}`)
                .waitFor({ state: "hidden" });
        }
    };

    if ("type" in expected) {
        await view.columnSettingsSidebar.container
            .locator(`.type-icon.${expected.type}`)
            .waitFor();
    }
    if ("unchanged" in expected) {
        if (!initial_state.open) {
            await expect(view.columnSettingsSidebar.container).toBeHidden({
                timeout: 1000,
            });
        } else {
            const unexpected_tabs = POSSIBLE_TABS.filter(
                (t) => !initial_state.tabs.includes(t),
            );
            await check_tabs(
                initial_state.selectedTab,
                initial_state.tabs,
                unexpected_tabs,
            );
        }
    } else if ("closed" in expected) {
        await expect(view.columnSettingsSidebar.container).toBeHidden({
            timeout: 1000,
        });
    } else {
        const unexpected_tabs = POSSIBLE_TABS.filter(
            (t) => !expected.tabs.includes(t),
        );
        await check_tabs(expected.selectedTab, expected.tabs, unexpected_tabs);
    }
}

test.beforeEach(async ({ page }) => {
    await page.goto("/rust/perspective-viewer/test/html/superstore-debug.html");
    await page.evaluate(async () => {
        while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) {
            await new Promise((x) => setTimeout(x, 10));
        }
    });
});

/**  These tests perform interactions on the viewer column selector and check for the intended behavior on the column settings sidebar.
 * They all start with the assumption that the sidebar is open.
 * NOTE: if we ever allow styles from an inactive tab, then these tests will need to accomodate for it.
 */
test.describe("Column Settings State on Interaction", () => {
    const excluded_actions: Record<string, Action[]> = {
        button: [
            "reorder",
            "aggregate",
            "groupby",
            "splitby",
            "orderby",
            "where",
        ],
        text: ["reorder", "open", "deactivate", "deaggregate"],
        dnd: ["open", "deactivate", "groupby", "splitby"],
    };

    for (const [name, spec] of Object.entries(TEST_SPEC)) {
        for (const [col_type, expected_output] of Object.entries(
            spec.outputs,
        )) {
            const expr = col_type === "expr_col";
            for (const action of spec.actions) {
                const name_prefix = `${name} - ${col_type} - ${action}`;

                if (!excluded_actions["button"].includes(action)) {
                    test(`${name_prefix} - Button`, async ({ page }) => {
                        let pageView = new PageView(page);
                        const [selector, snapshot_state] = await open(
                            pageView,
                            spec.initial_state,
                            expr,
                        );
                        switch (action) {
                            case "open": {
                                await selector.editBtn.click();
                                break;
                            }
                            case "activate": {
                                await selector.activeBtn.click();
                                break;
                            }
                            case "deactivate": {
                                await selector.activeBtn.click();
                                break;
                            }
                            case "deaggregate": {
                                await pageView.settingsPanel.groupby("Row ID");
                                await pageView.settingsPanel.removeViewParameter(
                                    "groupby",
                                    "Row ID",
                                );
                                break;
                            }
                            default: {
                                test.fail(true, "Unreachable");
                            }
                        }
                        await checkOutput(
                            pageView,
                            expected_output,
                            snapshot_state,
                        );
                    });
                }

                if (!excluded_actions["text"].includes(action)) {
                    test(`${name_prefix} - Text`, async ({ page }) => {
                        let pageView = new PageView(page);
                        const [selector, snapshot_state] = await open(
                            pageView,
                            spec.initial_state,
                            expr,
                        );
                        let selector_name = await selector.name.innerText();
                        switch (action) {
                            case "activate": {
                                await pageView.settingsPanel.activeColumns.activateColumn(
                                    selector_name,
                                );
                                break;
                            }
                            case "groupby": {
                                await pageView.settingsPanel.groupby(
                                    selector_name,
                                );
                                break;
                            }
                            case "splitby": {
                                await pageView.settingsPanel.splitby(
                                    selector_name,
                                );
                                break;
                            }
                            case "orderby": {
                                await pageView.settingsPanel.orderby(
                                    selector_name,
                                );
                                break;
                            }
                            case "where": {
                                await pageView.settingsPanel.where(
                                    selector_name,
                                );
                                break;
                            }
                            case "aggregate": {
                                await pageView.settingsPanel.groupby("Row ID");
                                break;
                            }
                            default: {
                                test.fail(true, "Unreachable");
                            }
                        }
                        await checkOutput(
                            pageView,
                            expected_output,
                            snapshot_state,
                        );
                    });
                }
                if (!excluded_actions["dnd"].includes(action)) {
                    test(`${name_prefix} - Drag'n'Drop`, async ({ page }) => {
                        let pageView = new PageView(page);
                        const [selector, snapshot_state] = await open(
                            pageView,
                            spec.initial_state,
                            expr,
                        );
                        switch (action) {
                            case "activate": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.activeColumns.columnSelector.last(),
                                );
                                break;
                            }
                            case "reorder": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.activeColumns.columnSelector.nth(
                                        1,
                                    ),
                                );
                                break;
                            }
                            case "groupby": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.groupbyInput,
                                );
                                break;
                            }
                            case "splitby": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.splitbyInput,
                                );
                                break;
                            }
                            case "orderby": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.orderbyInput,
                                );
                                break;
                            }
                            case "where": {
                                await selector.container.dragTo(
                                    pageView.settingsPanel.whereInput,
                                );
                                break;
                            }
                            case "aggregate": {
                                const aggregator =
                                    pageView.settingsPanel.inactiveColumns.columnSelector.first();
                                await aggregator.dragTo(
                                    pageView.settingsPanel.groupbyInput,
                                );
                                break;
                            }
                            case "deaggregate": {
                                let aggregator =
                                    pageView.settingsPanel.inactiveColumns.columnSelector.first();
                                await aggregator.dragTo(
                                    pageView.settingsPanel.groupbyInput,
                                );
                                aggregator = page
                                    .locator("#group_by .pivot-column")
                                    .first();
                                await aggregator.dragTo(
                                    pageView.settingsPanel.activeColumns.container
                                        .locator(".column-selector-column")
                                        .first(),
                                );
                            }
                        }
                        await checkOutput(
                            pageView,
                            expected_output,
                            snapshot_state,
                        );
                    });
                }
            }
        }
    }
});

test.describe("Unique Behaviors", () => {
    for (const destination of ["groupby", "splitby"]) {
        test(`${destination} - Single Active Column - table_col`, async ({
            page,
        }) => {
            const view = new PageView(page);
            const [selector, state_snapshot] = await open(
                view,
                { open: true, active: true, columns: ["City"] },
                false,
            );

            await selector.container.dragTo(
                destination === "groupby"
                    ? view.settingsPanel.groupbyInput
                    : view.settingsPanel.splitbyInput,
            );
            await checkOutput(
                view,
                destination == "groupby"
                    ? {
                          unchanged: true,
                          type: "integer",
                      }
                    : { unchanged: true },
                state_snapshot,
            );
        });
        test(`${destination} - Multiple Active Columns - table_col`, async ({
            page,
        }) => {
            const view = new PageView(page);
            const [selector, state_snapshot] = await open(
                view,
                { open: true, active: true, columns: ["City", "State"] },
                false,
            );

            await selector.container.dragTo(
                destination === "groupby"
                    ? view.settingsPanel.groupbyInput
                    : view.settingsPanel.splitbyInput,
            );
            // Dragging a table column onto group_by/split_by removes
            // it from `view_config.columns` (so `show_styles` is false
            // → no Style tab); a non-expression column has no
            // Attributes tab either, so the sidebar closes.
            await checkOutput(view, { closed: true }, state_snapshot);
        });
    }

    test("switching between date and datetime should rerender", async ({
        page,
    }) => {
        const view = new PageView(page);
        await view.restore({
            settings: true,
            plugin: "Debug Styled",
            columns: ["date", "datetime"],
            expressions: { date: "date(0,0,0)", datetime: "now()" },
        });
        const date = await view.settingsPanel.activeColumns.getColumnByName(
            "date",
            true,
        );
        await date.editBtn.click();
        await view.columnSettingsSidebar.openTab("Style");
        const dateSnapshot =
            await view.columnSettingsSidebar.styleTab.container.innerHTML();
        const datetime = await view.settingsPanel.activeColumns.getColumnByName(
            "datetime",
            true,
        );
        await datetime.editBtn.click();
        const datetimeSnapshot =
            await view.columnSettingsSidebar.styleTab.container.innerHTML();
        expect(datetimeSnapshot).not.toEqual(dateSnapshot);
    });
});
