import { render, screen, waitFor } from "@testing-library/react"
import { Effect, Layer } from "effect"
import { describe, expect, it, vi } from "vitest"
import * as Component from "../src/Component.js"
import * as ReactRuntime from "../src/ReactRuntime.js"


describe("Component", () => {
    it("runs useOnMount only once across rerenders", async () => {
        const onMount = vi.fn(() => Effect.succeed("mounted"))
        const runtime = ReactRuntime.make(Layer.empty)
        const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect)

        const Probe = Component.makeUntraced("UseOnMountProbe")(function*() {
            const value = yield* Component.useOnMount(onMount)
            return <div>{value}</div>
        }).pipe(
            Component.withRuntime(runtime.context)
        )

        const view = render(
            <runtime.context.Provider value={effectRuntime}>
                <Probe />
            </runtime.context.Provider>
        )

        await screen.findByText("mounted")
        expect(onMount).toHaveBeenCalledTimes(1)

        view.rerender(
            <runtime.context.Provider value={effectRuntime}>
                <Probe />
            </runtime.context.Provider>
        )
        expect(await screen.findByText("mounted")).toBeTruthy()
        expect(onMount).toHaveBeenCalledTimes(1)

        view.unmount()
        await Effect.runPromise(runtime.runtime.disposeEffect)
    })

    it("recomputes useOnChange only when dependencies change", async () => {
        const onChange = vi.fn((value: number) => Effect.succeed(`value:${value}`))
        const runtime = ReactRuntime.make(Layer.empty)
        const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect)

        const Probe = Component.makeUntraced("UseOnChangeProbe")(function*(props: { readonly value: number }) {
            const result = yield* Component.useOnChange(() => onChange(props.value), [props.value], {
                finalizerExecutionDebounce: 0,
            })

            return <div>{result}</div>
        }).pipe(
            Component.withRuntime(runtime.context)
        )

        const view = render(
            <runtime.context.Provider value={effectRuntime}>
                <Probe value={1} />
            </runtime.context.Provider>
        )

        await screen.findByText("value:1")
        expect(onChange).toHaveBeenCalledTimes(1)

        view.rerender(
            <runtime.context.Provider value={effectRuntime}>
                <Probe value={1} />
            </runtime.context.Provider>
        )

        expect(await screen.findByText("value:1")).toBeTruthy()
        expect(onChange).toHaveBeenCalledTimes(1)

        view.rerender(
            <runtime.context.Provider value={effectRuntime}>
                <Probe value={2} />
            </runtime.context.Provider>
        )

        await screen.findByText("value:2")
        expect(onChange).toHaveBeenCalledTimes(2)

        view.unmount()
        await Effect.runPromise(runtime.runtime.disposeEffect)
    })

    it("closes the previous scope on dependency changes and unmount", async () => {
        const cleanup = vi.fn()
        const runtime = ReactRuntime.make(Layer.empty)
        const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect)

        const Probe = Component.makeUntraced("ScopeCleanupProbe")(function*(props: { readonly value: string }) {
            const result = yield* Component.useOnChange(
                () => Effect.gen(function*() {
                    yield* Effect.addFinalizer(() => Effect.sync(() => cleanup(props.value)))
                    return props.value
                }),
                [props.value],
                { finalizerExecutionDebounce: 0 },
            )

            return <div>{result}</div>
        }).pipe(
            Component.withRuntime(runtime.context)
        )

        const view = render(
            <runtime.context.Provider value={effectRuntime}>
                <Probe value="first" />
            </runtime.context.Provider>
        )

        await screen.findByText("first")
        expect(cleanup).not.toHaveBeenCalled()

        view.rerender(
            <runtime.context.Provider value={effectRuntime}>
                <Probe value="second" />
            </runtime.context.Provider>
        )

        await screen.findByText("second")
        await waitFor(() => expect(cleanup).toHaveBeenCalledWith("first"))
        expect(cleanup).toHaveBeenCalledTimes(1)

        view.unmount()

        await waitFor(() => expect(cleanup).toHaveBeenCalledWith("second"))
        expect(cleanup).toHaveBeenCalledTimes(2)
        await Effect.runPromise(runtime.runtime.disposeEffect)
    })
})
