import { axisBox } from "../../../utils/geometry"
import { ResolvedValues, VisualElement } from "../../types"
import {
    AnimationState,
    AnimationType,
    createAnimationState,
} from "../animation-state"
import { checkTargetForNewValues, getOrigin } from "../setters"
import { visualElement } from "../../"
import { MotionProps } from "../../../motion"

const stateVisualElement = visualElement<
    ResolvedValues,
    {},
    { initialState: ResolvedValues }
>({
    createRenderState: () => ({}),
    build() {},
    measureViewportBox: axisBox,
    resetTransform() {},
    restoreTransform() {},
    removeValueFromMutableState() {},
    render() {},
    scrapeMotionValuesFromProps() {
        return {}
    },

    readValueFromInstance(_state, key, options) {
        return options.initialState[key] || 0
    },

    makeTargetAnimatable(element, { transition, transitionEnd, ...target }) {
        const origin = getOrigin(target as any, transition || {}, element)
        checkTargetForNewValues(element, target, origin as any)
        return { transition, transitionEnd, ...target }
    },
})

function createTest(
    props: MotionProps = {},
    parent: VisualElement<any> | undefined = undefined
): { element: VisualElement; state: any } {
    const element = stateVisualElement(
        { props, parent },
        {
            initialState: {},
        }
    )
    element.animationState = createAnimationState(element)

    return {
        element: element,
        state: {
            ...element.animationState,
            setProps(newProps: any, options: any, type: any): any {
                element.setProps(newProps)
                return element.animationState?.animateChanges(options, type)
            },
        },
    }
}

function mockAnimate(state: AnimationState) {
    const mocked = jest.fn()
    state.setAnimateFunction(() => {
        return (animations: any) => {
            mocked(animations.map(({ animation }: any) => animation))
        }
    })
    return mocked
}

describe("Animation state - Initiating props", () => {
    test("Initial animation", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            animate: { opacity: 1 },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).toBeCalledWith([{ opacity: 1 }])
    })

    test("Initial animation with prop as variant", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            animate: "test",
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).toBeCalledWith(["test"])
    })

    test("Initial animation with prop as variant list", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            animate: ["test", "heyoo"],
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).toBeCalledWith(["test", "heyoo"])
    })

    test("Initial animation with prop as inherited variant", () => {
        const { element: parent } = createTest({
            animate: "test",
        })
        const { state } = createTest({}, parent)

        const animate = mockAnimate(state)
        state.setProps({
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).not.toBeCalled()
    })

    test("Initial animation with initial=false", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            initial: false,
            animate: { opacity: 1 },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).not.toBeCalled()
    })

    test("Initial animation with prop as variant with initial=false", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            initial: false,
            animate: "test",
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).not.toBeCalled()
    })

    test("Initial animation with prop as variant list with initial=false", () => {
        const { state } = createTest()

        const animate = mockAnimate(state)
        state.setProps({
            initial: false,
            animate: ["test", "heyoo"],
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(animate).not.toBeCalled()
    })
})

describe("Animation state - Setting props", () => {
    test("No change, target", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: 1 },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: 1 },
        })

        expect(animate).not.toBeCalled()
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            opacity: true,
        })
    })
    test("No change, target keyframes", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: [0, 1] },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: [0, 1] },
        })

        expect(animate).not.toBeCalled()
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            opacity: true,
        })
    })

    test("No change, variant", () => {
        const { state } = createTest()

        state.setProps({
            animate: "test",
            variants: {
                test: { opacity: 1 },
            },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: "test",
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(animate).not.toBeCalled()
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            opacity: true,
        })
    })

    test("No change, variant list", () => {
        const { state } = createTest()

        state.setProps({
            animate: ["test", "test2"],
            variants: {
                test: { opacity: 1 },
            },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: ["test", "test2"],
            variants: {
                test: { opacity: 1 },
            },
        })

        expect(animate).not.toBeCalled()
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            opacity: true,
        })
    })

    test("Change single value, target", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: 1 },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: 0 },
        })

        expect(animate).toBeCalledWith([{ opacity: 0 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Change single value, keyframes", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: [0, 1] },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: [0.5, 1] },
        })

        expect(animate).toBeCalledWith([{ opacity: [0.5, 1] }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Change single value, variant", () => {
        const { state } = createTest()

        state.setProps({
            animate: "a",
            variants: {
                a: { opacity: 0 },
                b: { opacity: 1 },
            },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: "b",
            variants: {
                a: { opacity: 0 },
                b: { opacity: 1 },
            },
        })

        expect(animate).toBeCalledWith(["b"])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Change single value, variant list", () => {
        const { state } = createTest()

        state.setProps({
            animate: ["a"],
            variants: {
                a: { opacity: 0 },
                b: { opacity: 1 },
            },
        })

        const animate = mockAnimate(state)

        state.setProps({
            animate: ["b"],
            variants: {
                a: { opacity: 0 },
                b: { opacity: 1 },
            },
        })

        expect(animate).toBeCalledWith(["b"])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Swap between value in target and transitionEnd, target", () => {
        const { state } = createTest()

        state.setProps({
            style: { opacity: 0.1 },
            animate: { opacity: 0.2 },
        })

        let animate = mockAnimate(state)

        state.setProps({
            style: { opacity: 0.1 },
            animate: { transitionEnd: { opacity: 0.3 } },
        })

        expect(animate).toBeCalledWith([{ transitionEnd: { opacity: 0.3 } }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )

        animate = mockAnimate(state)

        state.setProps({
            style: { opacity: 0.1 },
            animate: { opacity: 0.2 },
        })
        expect(animate).toBeCalledWith([{ opacity: 0.2 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Change single value, target, with unchanging values", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: 1, x: 0 },
        })

        let animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: 0, x: 0 },
        })

        expect(animate).toBeCalledWith([{ opacity: 0, x: 0 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            x: true,
        })

        animate = mockAnimate(state)

        state.setProps({
            animate: { opacity: 0, x: 100 },
        })

        expect(animate).toBeCalledWith([{ opacity: 0, x: 100 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
            opacity: true,
        })
    })

    test("Removing values, target changed", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: 1 },
        })

        const animate = mockAnimate(state)

        state.setProps({
            style: { opacity: 0 },
            animate: {},
        })

        expect(animate).toBeCalledWith([{ opacity: 0 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Removing values, target undefined", () => {
        const { state } = createTest()

        state.setProps({
            animate: { opacity: 1 },
        })

        const animate = mockAnimate(state)

        state.setProps({
            style: { opacity: 0 },
            animate: undefined,
        })

        expect(animate).toBeCalledWith([{ opacity: 0 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    /**
     * TODO: Un-skip these and validate before merge
     */
    test.skip("Removing values, variant changed", () => {
        const { state } = createTest()

        state.setProps({
            animate: "a",
            variants: {
                a: { opacity: 0 },
                b: { x: 1 },
            },
        })

        const animate = mockAnimate(state)

        state.setProps({
            style: { opacity: 1 },
            animate: "b",
            variants: {
                a: { opacity: 0 },
                b: { x: 1 },
            },
        })

        expect(animate).toBeCalledWith(["b", { opacity: 1 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test.skip("Removing values, inherited variant changed", () => {
        const { element: parent } = createTest({ animate: "a" })
        const { state } = createTest({}, parent)

        state.setProps({
            variants: {
                a: { opacity: 0 },
                b: { x: 1 },
            },
        })

        const animate = mockAnimate(state)
        parent.setProps({ animate: "b" })
        state.setProps({
            style: { opacity: 1 },
            variants: {
                a: { opacity: 0 },
                b: { x: 1 },
            },
        })

        expect(animate).toBeCalledWith([{ opacity: 1 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })
})

describe("Animation state - Set active", () => {
    test("Change active state while props are the same", () => {
        const { state } = createTest()

        state.setProps({
            style: { opacity: 0 },
            animate: { opacity: 1 },
            whileHover: { opacity: 0.5 },
            whileTap: { opacity: 0.8 },
        })

        // Set hover to true
        let animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, true)
        expect(animate).toBeCalledWith([{ opacity: 0.5 }])
        expect(
            state.getState()[AnimationType.Animate].protectedKeys
        ).toHaveProperty("opacity")
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set hover to false
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, false)
        expect(animate).toBeCalledWith([{ opacity: 1 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set hover to true
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, true)
        expect(animate).toBeCalledWith([{ opacity: 0.5 }])
        expect(
            state.getState()[AnimationType.Animate].protectedKeys
        ).toHaveProperty("opacity")
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set hover to false
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, false)
        expect(animate).toBeCalledWith([{ opacity: 1 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set hover to true
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, true)
        expect(animate).toBeCalledWith([{ opacity: 0.5 }])
        expect(
            state.getState()[AnimationType.Animate].protectedKeys
        ).toHaveProperty("opacity")
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set press to true
        animate = mockAnimate(state)
        state.setActive(AnimationType.Tap, true)
        expect(animate).toBeCalledWith([{ opacity: 0.8 }])
        expect(
            state.getState()[AnimationType.Animate].protectedKeys
        ).toHaveProperty("opacity")
        expect(
            state.getState()[AnimationType.Hover].protectedKeys
        ).toHaveProperty("opacity")
        expect(state.getState()[AnimationType.Tap].protectedKeys).toEqual({})

        // Set hover to false
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, false)
        expect(
            state.getState()[AnimationType.Animate].protectedKeys
        ).toHaveProperty("opacity")
        expect(
            state.getState()[AnimationType.Tap].protectedKeys
        ).toHaveProperty("opacity")
        expect(animate).not.toBeCalled()

        // Set press to false
        animate = mockAnimate(state)
        state.setActive(AnimationType.Tap, false)
        expect(animate).toBeCalledWith([{ opacity: 1 }])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
    })

    test("Change active variant where no variants are defined", () => {
        /**
         * We should still animate back to lower-priority variants in case children need animating to
         */
        const { state } = createTest()

        state.setProps({
            animate: "a",
            whileHover: "b",
        })

        // Set hover to true
        let animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, true)
        expect(animate).toBeCalledWith(["b"])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})

        // Set hover to false
        animate = mockAnimate(state)
        state.setActive(AnimationType.Hover, false)
        expect(animate).toBeCalledWith(["a"])
        expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
            {}
        )
        expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
    })
})
