/*
 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { mount } from "enzyme";

import { GraphIcon, type IconName, Icons, IconSize } from "@blueprintjs/icons";
import { Add, Airplane, Calendar, Graph } from "@blueprintjs/icons/lib/cjs/generated/16px/paths";
import { afterEach, beforeAll, describe, expect, it, type MockInstance, vi } from "@blueprintjs/test-commons/vitest";

import { Classes, DISPLAYNAME_PREFIX, Intent } from "../../common";

import { Icon, type IconProps } from "./icon";

describe("<Icon>", () => {
    let iconLoader: MockInstance;

    beforeAll(() => {
        vi.spyOn(Icons, "load").mockResolvedValue(undefined);
        // stub the dynamic icon loader with a synchronous, static one
        iconLoader = vi.spyOn(Icons, "getPaths").mockImplementation((name: string) => {
            switch (name) {
                case "add":
                    return Add;
                case "airplane":
                    return Airplane;
                case "calendar":
                    return Calendar;
                case "graph":
                    return Graph;
                default:
                    return undefined;
            }
        });
    });

    afterEach(() => {
        iconLoader?.mockClear();
    });

    it("tagName dictates HTML tag", async () => {
        const wrapper = mount(<Icon icon="calendar" tagName="i" />);
        wrapper.update();
        expect(wrapper.find("i").exists()).toBe(true);
    });

    it("size=16 renders standard size", async () =>
        assertIconSize(<Icon icon="graph" size={IconSize.STANDARD} />, IconSize.STANDARD));

    it("size=20 renders large size", async () =>
        assertIconSize(<Icon icon="graph" size={IconSize.LARGE} />, IconSize.LARGE));

    it("forwards size onto an element icon", async () =>
        assertIconSize(<Icon icon={<GraphIcon />} size={IconSize.LARGE} />, IconSize.LARGE));

    it("forwards a custom size onto an element icon", async () =>
        assertIconSize(<Icon icon={<GraphIcon />} size={128} />, 128));

    it("element icon's own size takes precedence over the <Icon> size", async () =>
        // non-regression: the element's explicit size wins over the forwarded one
        assertIconSize(<Icon icon={<GraphIcon size={IconSize.STANDARD} />} size={128} />, IconSize.STANDARD));

    it("element icon without a size falls back to the default size", async () =>
        assertIconSize(<Icon icon={<GraphIcon />} />, IconSize.STANDARD));

    it("renders intent class", async () => {
        const wrapper = mount(<Icon icon="add" intent={Intent.DANGER} />);
        expect(wrapper.find(`.${Classes.INTENT_DANGER}`).exists()).toBe(true);
    });

    it.skip("renders icon name", async () => {
        assertIconHasPath(<Icon icon="calendar" />, "calendar");
    });

    it("renders icon without color", async () => {
        assertIconColor(<Icon icon="add" />);
    });

    it("renders icon color", async () => {
        assertIconColor(<Icon icon="add" color="red" />, "red");
    });

    it("forwards color onto an element icon", async () => {
        assertIconColor(<Icon icon={<GraphIcon />} color="red" />, "red");
    });

    it("element icon's own color takes precedence over the <Icon> color", async () =>
        // non-regression: the element's explicit color wins over the forwarded one
        assertIconColor(<Icon icon={<GraphIcon color="blue" />} color="red" />, "blue"));

    // A non-Blueprint element used as an icon: it renders its own <svg> reflecting only the props it
    // receives, so the assertions below can prove that size/color were NOT forwarded onto it.
    function CustomIcon(elementProps: { className?: string; color?: string; size?: number }) {
        return (
            <svg
                className={elementProps.className}
                fill={elementProps.color}
                height={elementProps.size}
                width={elementProps.size}
            />
        );
    }
    CustomIcon.displayName = "CustomIcon";

    it("does not forward size/color onto a non-Blueprint element icon, but merges className and intent", () => {
        const wrapper = mount(
            <Icon icon={<CustomIcon />} size={IconSize.LARGE} color="red" className="custom" intent={Intent.DANGER} />,
        );
        wrapper.update();
        const svg = wrapper.find("svg");
        // gated: size/color are not forwarded onto a non-Blueprint element
        expect(svg.prop("width")).toBeUndefined();
        expect(svg.prop("height")).toBeUndefined();
        expect(svg.prop("fill")).toBeUndefined();
        // className + intent class are still merged onto element icons of any type (issue #8080)
        expect(svg.hasClass("custom")).toBe(true);
        expect(svg.hasClass(Classes.INTENT_DANGER)).toBe(true);
    });

    it("does not forward size/color onto a host-element icon, but merges className and intent", () => {
        const wrapper = mount(
            <Icon icon={<article />} size={IconSize.LARGE} color="red" className="custom" intent={Intent.DANGER} />,
        );
        wrapper.update();
        const article = wrapper.find("article");
        expect(article.prop("size")).toBeUndefined();
        expect(article.prop("color")).toBeUndefined();
        expect(article.hasClass("custom")).toBe(true);
        expect(article.hasClass(Classes.INTENT_DANGER)).toBe(true);
    });

    it("generated icon components carry the displayName prefix that size/color gating relies on", () => {
        // Guards against core's DISPLAYNAME_PREFIX and the @blueprintjs/icons generators drifting apart,
        // which would silently disable size/color forwarding onto element icons.
        expect(GraphIcon.displayName).toBeDefined();
        expect(GraphIcon.displayName!.startsWith(`${DISPLAYNAME_PREFIX}.Icon.`)).toBe(true);
    });

    it("unknown icon name renders blank icon", async () => {
        const wrapper = mount(<Icon icon={"unknown" as any} />);
        wrapper.update();
        expect(wrapper.find("path")).toHaveLength(0);
    });

    it("prefixed icon renders blank icon", async () => {
        const wrapper = mount(<Icon icon={Classes.iconClass("airplane") as any} />);
        wrapper.update();
        expect(wrapper.find("path")).toHaveLength(0);
    });

    it("icon element passes through unchanged", async () => {
        // NOTE: This is supported to simplify usage of this component in other
        // Blueprint components which accept `icon?: IconName | React.JSX.Element`.
        const onClick = () => true;
        const wrapper = mount(<Icon icon={<article onClick={onClick} />} />);
        wrapper.update();
        expect(wrapper.childAt(0).is("article")).toBe(true);
        expect(wrapper.find("article").prop("onClick")).toBe(onClick);
        // a forwarded `size` should not leak onto a non-icon element when none is provided
        expect(wrapper.find("article").prop("size")).toBeUndefined();
    });

    it("icon=undefined renders nothing", async () => {
        const wrapper = mount(<Icon icon={undefined} />);
        wrapper.update();
        expect(wrapper.isEmptyRender()).toBe(true);
    });

    it("title sets content of <title> element", async () => {
        const wrapper = mount(<Icon icon="airplane" title="bird" />);
        wrapper.update();
        expect(wrapper.find("title").text()).toBe("bird");
    });

    it("does not add desc if title is not provided", () => {
        const icon = mount(<Icon icon="airplane" />);
        expect(icon.find("desc")).toHaveLength(0);
    });

    it("applies aria-hidden=true if title is not defined", () => {
        const icon = mount(<Icon icon="airplane" />);
        expect(icon.find(`.${Classes.ICON}`).hostNodes().prop("aria-hidden")).toBe(true);
    });

    it("supports mouse event handlers of type React.MouseEventHandler", () => {
        const handleClick: React.MouseEventHandler = () => undefined;
        mount(<Icon icon="add" onClick={handleClick} />);
    });

    it("accepts HTML attributes", () => {
        mount(<Icon<HTMLSpanElement> icon="drag-handle-vertical" draggable={false} />);
    });

    it("accepts generic type param specifying the type of the root element", () => {
        const handleClick: React.MouseEventHandler<HTMLSpanElement> = () => undefined;
        mount(<Icon<HTMLSpanElement> icon="add" onClick={handleClick} />);
    });

    it("allows specifying the root element as <svg> when tagName={null}", () => {
        const handleClick: React.MouseEventHandler<SVGSVGElement> = () => undefined;
        const wrapper = mount(<Icon<SVGSVGElement> icon="add" onClick={handleClick} tagName={null} />);
        expect(wrapper.find("span").exists()).toBe(false);
    });

    /** Asserts that rendered icon has an SVG path. */
    async function assertIconHasPath(icon: React.ReactElement<IconProps>, iconName: IconName) {
        const wrapper = mount(icon);
        wrapper.update();
        expect(wrapper.text()).toBe(iconName);
        expect(wrapper.find("path").length, "should find at least one path element").toBeGreaterThan(0);
    }

    /** Asserts that rendered icon has width/height equal to size. */
    async function assertIconSize(icon: React.ReactElement<IconProps>, size: number) {
        const wrapper = mount(icon);
        wrapper.update();
        const svg = wrapper.find("svg");
        expect(svg.prop("width")).toBe(size);
        expect(svg.prop("height")).toBe(size);
    }

    /** Asserts that rendered icon has color equal to color. */
    async function assertIconColor(icon: React.ReactElement<IconProps>, color?: string) {
        const wrapper = mount(icon);
        wrapper.update();
        const svg = wrapper.find("svg");
        expect(svg.prop("fill")).toEqual(color);
    }
});
