"use strict"

const JASMINE_SESSION_ID = `jasmine-${String(Date.now()).slice(8)}`

beforeEach(function() {
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 60 * 1000 // Test timeout after 15 minutes
    jasmine.addMatchers({
        toBeOneOf: function() {
            return {
                compare: function(actual, expected) {
                    return {
                        pass: expected.includes(actual),
                    }
                },
            }
        },
    })
})
describe("stable-diffusion-ui", function() {
    beforeEach(function() {
        expect(typeof SD).toBe("object")
        expect(typeof SD.serverState).toBe("object")
        expect(typeof SD.serverState.status).toBe("string")
    })
    it("should be able to reach the backend", async function() {
        expect(SD.serverState.status).toBe(SD.ServerStates.unavailable)
        SD.sessionId = JASMINE_SESSION_ID
        await SD.init()
        expect(SD.isServerAvailable()).toBeTrue()
    })

    it("enfore the current task state", function() {
        const task = new SD.Task()
        expect(task.status).toBe(SD.TaskStatus.init)
        expect(task.isPending).toBeTrue()

        task._setStatus(SD.TaskStatus.pending)
        expect(task.status).toBe(SD.TaskStatus.pending)
        expect(task.isPending).toBeTrue()
        expect(function() {
            task._setStatus(SD.TaskStatus.init)
        }).toThrowError()

        task._setStatus(SD.TaskStatus.waiting)
        expect(task.status).toBe(SD.TaskStatus.waiting)
        expect(task.isPending).toBeTrue()
        expect(function() {
            task._setStatus(SD.TaskStatus.pending)
        }).toThrowError()

        task._setStatus(SD.TaskStatus.processing)
        expect(task.status).toBe(SD.TaskStatus.processing)
        expect(task.isPending).toBeTrue()
        expect(function() {
            task._setStatus(SD.TaskStatus.pending)
        }).toThrowError()

        task._setStatus(SD.TaskStatus.failed)
        expect(task.status).toBe(SD.TaskStatus.failed)
        expect(task.isPending).toBeFalse()
        expect(function() {
            task._setStatus(SD.TaskStatus.processing)
        }).toThrowError()
        expect(function() {
            task._setStatus(SD.TaskStatus.completed)
        }).toThrowError()
    })
    it("should be able to run tasks", async function() {
        expect(typeof SD.Task.run).toBe("function")
        const promiseGenerator = (function*(val) {
            expect(val).toBe("start")
            expect(yield 1 + 1).toBe(4)
            expect(yield 2 + 2).toBe(8)
            yield asyncDelay(500)
            expect(yield 3 + 3).toBe(12)
            expect(yield 4 + 4).toBe(16)
            return 8 + 8
        })("start")
        const callback = function({ value, done }) {
            return { value: 2 * value, done }
        }
        expect(await SD.Task.run(promiseGenerator, { callback })).toBe(32)
    })
    it("should be able to queue tasks", async function() {
        expect(typeof SD.Task.enqueue).toBe("function")
        const promiseGenerator = (function*(val) {
            expect(val).toBe("start")
            expect(yield 1 + 1).toBe(4)
            expect(yield 2 + 2).toBe(8)
            yield asyncDelay(500)
            expect(yield 3 + 3).toBe(12)
            expect(yield 4 + 4).toBe(16)
            return 8 + 8
        })("start")
        const callback = function({ value, done }) {
            return { value: 2 * value, done }
        }
        const gen = SD.Task.asGenerator({ generator: promiseGenerator, callback })
        expect(await SD.Task.enqueue(gen)).toBe(32)
    })
    it("should be able to chain handlers", async function() {
        expect(typeof SD.Task.enqueue).toBe("function")
        const promiseGenerator = (function*(val) {
            expect(val).toBe("start")
            expect(yield { test: "1" }).toEqual({ test: "1", foo: "bar" })
            expect(yield 2 + 2).toEqual(8)
            yield asyncDelay(500)
            expect(yield 3 + 3).toEqual(12)
            expect(yield { test: 4 }).toEqual({ test: 8, foo: "bar" })
            return { test: 8 }
        })("start")
        const gen1 = SD.Task.asGenerator({
            generator: promiseGenerator,
            callback: function({ value, done }) {
                if (typeof value === "object") {
                    value["foo"] = "bar"
                }
                return { value, done }
            },
        })
        const gen2 = SD.Task.asGenerator({
            generator: gen1,
            callback: function({ value, done }) {
                if (typeof value === "number") {
                    value = 2 * value
                }
                if (typeof value === "object" && typeof value.test === "number") {
                    value.test = 2 * value.test
                }
                return { value, done }
            },
        })
        expect(await SD.Task.enqueue(gen2)).toEqual({ test: 32, foo: "bar" })
    })
    describe("ServiceContainer", function() {
        it("should be able to register providers", function() {
            const cont = new ServiceContainer(
                function foo() {
                    this.bar = ""
                },
                function bar() {
                    return () => 0
                },
                { name: "zero", definition: 0 },
                { name: "ctx", definition: () => Object.create(null), singleton: true },
                {
                    name: "test",
                    definition: (ctx, missing, one, foo) => {
                        expect(ctx).toEqual({ ran: true })
                        expect(one).toBe(1)
                        expect(typeof foo).toBe("object")
                        expect(foo.bar).toBeDefined()
                        expect(typeof missing).toBe("undefined")
                        return { foo: "bar" }
                    },
                    dependencies: ["ctx", "missing", "one", "foo"],
                }
            )
            const fooObj = cont.get("foo")
            expect(typeof fooObj).toBe("object")
            fooObj.ran = true

            const ctx = cont.get("ctx")
            expect(ctx).toEqual({})
            ctx.ran = true

            const bar = cont.get("bar")
            expect(typeof bar).toBe("function")
            expect(bar()).toBe(0)

            cont.register({ name: "one", definition: 1 })
            const test = cont.get("test")
            expect(typeof test).toBe("object")
            expect(test.foo).toBe("bar")
        })
    })
    it("should be able to stream data in chunks", async function() {
        expect(SD.isServerAvailable()).toBeTrue()
        const nbr_steps = 15
        let res = await fetch("/render", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                prompt: "a photograph of an astronaut riding a horse",
                negative_prompt: "",
                width: 128,
                height: 128,
                seed: Math.floor(Math.random() * 10000000),

                sampler: "plms",
                use_stable_diffusion_model: "sd-v1-4",
                num_inference_steps: nbr_steps,
                guidance_scale: 7.5,

                numOutputsParallel: 1,
                stream_image_progress: true,
                show_only_filtered_image: true,
                output_format: "jpeg",

                session_id: JASMINE_SESSION_ID,
            }),
        })
        expect(res.ok).toBeTruthy()
        const renderRequest = await res.json()
        expect(typeof renderRequest.stream).toBe("string")
        expect(renderRequest.task).toBeDefined()

        // Wait for server status to update.
        await SD.waitUntil(
            () => {
                console.log("Waiting for %s to be received...", renderRequest.task)
                return !SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)]
            },
            250,
            10 * 60 * 1000
        )
        // Wait for task to start on server.
        await SD.waitUntil(() => {
            console.log("Waiting for %s to start...", renderRequest.task)
            return !SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)] !== "pending"
        }, 250)

        const reader = new SD.ChunkedStreamReader(renderRequest.stream)
        const parseToString = reader.parse
        reader.parse = function(value) {
            value = parseToString.call(this, value)
            if (!value || value.length <= 0) {
                return
            }
            return reader.readStreamAsJSON(value.join(""))
        }
        reader.onNext = function({ done, value }) {
            console.log(value)
            if (typeof value === "object" && "status" in value) {
                done = true
            }
            return { done, value }
        }
        let lastUpdate = undefined
        let stepCount = 0
        let complete = false
        //for await (const stepUpdate of reader) {
        for await (const stepUpdate of reader.open()) {
            console.log("ChunkedStreamReader received ", stepUpdate)
            lastUpdate = stepUpdate
            if (complete) {
                expect(stepUpdate.status).toBe("succeeded")
                expect(stepUpdate.output).toHaveSize(1)
            } else {
                expect(stepUpdate.total_steps).toBe(nbr_steps)
                expect(stepUpdate.step).toBe(stepCount)
                if (stepUpdate.step === stepUpdate.total_steps) {
                    complete = true
                } else {
                    stepCount++
                }
            }
        }
        for (let i = 1; i <= 5; ++i) {
            res = await fetch(renderRequest.stream)
            expect(res.ok).toBeTruthy()
            const cachedResponse = await res.json()
            console.log("Cache test %s received %o", i, cachedResponse)
            expect(lastUpdate).toEqual(cachedResponse)
        }
    })

    describe("should be able to make renders", function() {
        beforeEach(function() {
            expect(SD.isServerAvailable()).toBeTrue()
        })
        it("basic inline request", async function() {
            let stepCount = 0
            let complete = false
            const result = await SD.render(
                {
                    prompt: "a photograph of an astronaut riding a horse",
                    width: 128,
                    height: 128,
                    num_inference_steps: 10,
                    show_only_filtered_image: false,
                    //"use_face_correction": 'GFPGANv1.3',
                    use_upscale: "RealESRGAN_x4plus",
                    session_id: JASMINE_SESSION_ID,
                },
                function(event) {
                    console.log(this, event)
                    if ("update" in event) {
                        const stepUpdate = event.update
                        if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
                            expect(stepUpdate.status).toBe("succeeded")
                            expect(stepUpdate.output).toHaveSize(2)
                        } else {
                            expect(stepUpdate.step).toBe(stepCount)
                            if (stepUpdate.step === stepUpdate.total_steps) {
                                complete = true
                            } else {
                                stepCount++
                            }
                        }
                    }
                }
            )
            console.log(result)
            expect(result.status).toBe("succeeded")
            expect(result.output).toHaveSize(2)
        })
        it("post and reader request", async function() {
            const renderTask = new SD.RenderTask({
                prompt: "a photograph of an astronaut riding a horse",
                width: 128,
                height: 128,
                seed: SD.MAX_SEED_VALUE,
                num_inference_steps: 10,
                session_id: JASMINE_SESSION_ID,
            })
            expect(renderTask.status).toBe(SD.TaskStatus.init)

            const timeout = -1
            const renderRequest = await renderTask.post(timeout)
            expect(typeof renderRequest.stream).toBe("string")
            expect(renderTask.status).toBe(SD.TaskStatus.waiting)
            expect(renderTask.streamUrl).toBe(renderRequest.stream)

            await renderTask.waitUntil({
                state: SD.TaskStatus.processing,
                callback: () => console.log("Waiting for render task to start..."),
            })
            expect(renderTask.status).toBe(SD.TaskStatus.processing)

            let stepCount = 0
            let complete = false
            //for await (const stepUpdate of renderTask.reader) {
            for await (const stepUpdate of renderTask.reader.open()) {
                console.log(stepUpdate)
                if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
                    expect(stepUpdate.status).toBe("succeeded")
                    expect(stepUpdate.output).toHaveSize(1)
                } else {
                    expect(stepUpdate.step).toBe(stepCount)
                    if (stepUpdate.step === stepUpdate.total_steps) {
                        complete = true
                    } else {
                        stepCount++
                    }
                }
            }
            expect(renderTask.status).toBe(SD.TaskStatus.completed)
            expect(renderTask.result.status).toBe("succeeded")
            expect(renderTask.result.output).toHaveSize(1)
        })
        it("queued request", async function() {
            let stepCount = 0
            let complete = false
            const renderTask = new SD.RenderTask({
                prompt: "a photograph of an astronaut riding a horse",
                width: 128,
                height: 128,
                num_inference_steps: 10,
                show_only_filtered_image: false,
                //"use_face_correction": 'GFPGANv1.3',
                use_upscale: "RealESRGAN_x4plus",
                session_id: JASMINE_SESSION_ID,
            })
            await renderTask.enqueue(function(event) {
                console.log(this, event)
                if ("update" in event) {
                    const stepUpdate = event.update
                    if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
                        expect(stepUpdate.status).toBe("succeeded")
                        expect(stepUpdate.output).toHaveSize(2)
                    } else {
                        expect(stepUpdate.step).toBe(stepCount)
                        if (stepUpdate.step === stepUpdate.total_steps) {
                            complete = true
                        } else {
                            stepCount++
                        }
                    }
                }
            })
            console.log(renderTask.result)
            expect(renderTask.result.status).toBe("succeeded")
            expect(renderTask.result.output).toHaveSize(2)
        })
    })
    describe("# Special cases", function() {
        it("should throw an exception on set for invalid sessionId", function() {
            expect(function() {
                SD.sessionId = undefined
            }).toThrowError("Can't set sessionId to undefined.")
        })
    })
})

const loadCompleted = window.onload
let loadEvent = undefined
window.onload = function(evt) {
    loadEvent = evt
}
if (!PLUGINS.SELFTEST) {
    PLUGINS.SELFTEST = {}
}
loadUIPlugins().then(function() {
    console.log("loadCompleted", loadEvent)
    describe("@Plugins", function() {
        it("exposes hooks to overide", function() {
            expect(typeof PLUGINS.IMAGE_INFO_BUTTONS).toBe("object")
            expect(typeof PLUGINS.TASK_CREATE).toBe("object")
        })
        describe("supports selftests", function() {
            // Hook to allow plugins to define tests.
            const pluginsTests = Object.keys(PLUGINS.SELFTEST).filter((key) => PLUGINS.SELFTEST.hasOwnProperty(key))
            if (!pluginsTests || pluginsTests.length <= 0) {
                it("but nothing loaded...", function() {
                    expect(true).toBeTruthy()
                })
                return
            }
            for (const pTest of pluginsTests) {
                describe(pTest, function() {
                    const testFn = PLUGINS.SELFTEST[pTest]
                    return Promise.resolve(testFn.call(jasmine, pTest))
                })
            }
        })
    })
    loadCompleted.call(window, loadEvent)
})
