/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

// @vitest-environment happy-dom

import { vi, describe, it, expect, beforeEach } from "vitest";
import fetchMock from "@fetch-mock/vitest";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { stubClient } from "test-utils";

import ScalarAuthClient from "./ScalarAuthClient";
import SdkConfig from "./SdkConfig";
import { WidgetType } from "./widgets/WidgetType";

describe("ScalarAuthClient", function () {
    const apiUrl = "https://test.com/api";
    const uiUrl = "https:/test.com/app";
    const tokenObject = {
        access_token: "token",
        token_type: "Bearer",
        matrix_server_name: "localhost",
        expires_in: 999,
    };

    let client: MatrixClient;
    beforeEach(function () {
        vi.clearAllMocks();
        client = stubClient();
    });

    it("should request a new token if the old one fails", async function () {
        const sac = new ScalarAuthClient(apiUrl + 0, uiUrl);

        fetchMock.get("https://test.com/api0/account?scalar_token=brokentoken&v=1.1", {
            body: { message: "Invalid token" },
        });

        fetchMock.get("https://test.com/api0/account?scalar_token=wokentoken&v=1.1", {
            body: { user_id: client.getUserId() },
        });

        client.getOpenIdToken = vi.fn().mockResolvedValue(tokenObject);

        sac.exchangeForScalarToken = vi.fn((arg) => {
            return Promise.resolve(arg === tokenObject ? "wokentoken" : "othertoken");
        });

        await sac.connect();

        expect(sac.exchangeForScalarToken).toHaveBeenCalledWith(tokenObject);
        expect(sac.hasCredentials).toBeTruthy();
        // @ts-ignore private property
        expect(sac.scalarToken).toEqual("wokentoken");
    });

    describe("exchangeForScalarToken", () => {
        it("should return `scalar_token` from API /register", async () => {
            const sac = new ScalarAuthClient(apiUrl + 1, uiUrl);

            fetchMock.postOnce("https://test.com/api1/register?v=1.1", {
                body: { scalar_token: "stoken" },
            });

            await expect(sac.exchangeForScalarToken(tokenObject)).resolves.toBe("stoken");
        });

        it("should throw upon non-20x code", async () => {
            const sac = new ScalarAuthClient(apiUrl + 2, uiUrl);

            fetchMock.postOnce("https://test.com/api2/register?v=1.1", {
                status: 500,
            });

            await expect(sac.exchangeForScalarToken(tokenObject)).rejects.toThrow("Scalar request failed: 500");
        });

        it("should throw if scalar_token is missing in response", async () => {
            const sac = new ScalarAuthClient(apiUrl + 3, uiUrl);

            fetchMock.postOnce("https://test.com/api3/register?v=1.1", {
                body: {},
            });

            await expect(sac.exchangeForScalarToken(tokenObject)).rejects.toThrow("Missing scalar_token in response");
        });
    });

    describe("registerForToken", () => {
        it("should call `termsInteractionCallback` upon M_TERMS_NOT_SIGNED error", async () => {
            const sac = new ScalarAuthClient(apiUrl + 4, uiUrl);
            const termsInteractionCallback = vi.fn();
            sac.setTermsInteractionCallback(termsInteractionCallback);
            fetchMock.get("https://test.com/api4/account?scalar_token=testtoken1&v=1.1", {
                body: { errcode: "M_TERMS_NOT_SIGNED" },
            });
            sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken1"));
            vi.mocked(client.getTerms).mockResolvedValue({ policies: {} });

            await expect(sac.registerForToken()).resolves.toBe("testtoken1");
        });

        it("should throw upon non-20x code", async () => {
            const sac = new ScalarAuthClient(apiUrl + 5, uiUrl);
            fetchMock.get("https://test.com/api5/account?scalar_token=testtoken2&v=1.1", {
                body: { errcode: "SERVER_IS_SAD" },
                status: 500,
            });
            sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken2"));

            await expect(sac.registerForToken()).rejects.toBeTruthy();
        });

        it("should throw if user_id is missing from response", async () => {
            const sac = new ScalarAuthClient(apiUrl + 6, uiUrl);
            fetchMock.get("https://test.com/api6/account?scalar_token=testtoken3&v=1.1", {
                body: {},
            });
            sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken3"));

            await expect(sac.registerForToken()).rejects.toThrow("Missing user_id in response");
        });
    });

    describe("getScalarPageTitle", () => {
        let sac: ScalarAuthClient;

        beforeEach(async () => {
            SdkConfig.put({
                integrations_rest_url: apiUrl + 7,
                integrations_ui_url: uiUrl,
            });

            window.localStorage.setItem("mx_scalar_token_at_https://test.com/api7", "wokentoken1");
            fetchMock.get("https://test.com/api7/account?scalar_token=wokentoken1&v=1.1", {
                body: { user_id: client.getUserId() },
            });

            sac = new ScalarAuthClient(apiUrl + 7, uiUrl);
            await sac.connect();
        });

        it("should return `cached_title` from API /widgets/title_lookup", async () => {
            const url = "google.com";
            fetchMock.get("https://test.com/api7/widgets/title_lookup?scalar_token=wokentoken1&curl=" + url, {
                body: {
                    page_title_cache_item: {
                        cached_title: "Google",
                    },
                },
            });

            await expect(sac.getScalarPageTitle(url)).resolves.toBe("Google");
        });

        it("should throw upon non-20x code", async () => {
            const url = "yahoo.com";
            fetchMock.get("https://test.com/api7/widgets/title_lookup?scalar_token=wokentoken1&curl=" + url, {
                status: 500,
            });

            await expect(sac.getScalarPageTitle(url)).rejects.toThrow("Scalar request failed: 500");
        });
    });

    describe("disableWidgetAssets", () => {
        let sac: ScalarAuthClient;

        beforeEach(async () => {
            SdkConfig.put({
                integrations_rest_url: apiUrl + 8,
                integrations_ui_url: uiUrl,
            });

            window.localStorage.setItem("mx_scalar_token_at_https://test.com/api8", "wokentoken1");
            fetchMock.get("https://test.com/api8/account?scalar_token=wokentoken1&v=1.1", {
                body: { user_id: client.getUserId() },
            });

            sac = new ScalarAuthClient(apiUrl + 8, uiUrl);
            await sac.connect();
        });

        it("should send state=disable to API /widgets/set_assets_state", async () => {
            fetchMock.get(
                "https://test.com/api8/widgets/set_assets_state?scalar_token=wokentoken1" +
                    "&widget_type=m.custom&widget_id=id1&state=disable",
                {
                    body: "OK",
                },
            );

            await expect(sac.disableWidgetAssets(WidgetType.CUSTOM, "id1")).resolves.toBeUndefined();
        });

        it("should throw upon non-20x code", async () => {
            fetchMock.get(
                "https://test.com/api8/widgets/set_assets_state?scalar_token=wokentoken1" +
                    "&widget_type=m.custom&widget_id=id2&state=disable",
                {
                    status: 500,
                },
            );

            await expect(sac.disableWidgetAssets(WidgetType.CUSTOM, "id2")).rejects.toThrow(
                "Scalar request failed: 500",
            );
        });
    });
});
