/*
 * Copyright 2004-present the original author or authors.
 *
 * 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
 *
 *      https://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.
 */

"use strict";

import "./bootstrap.js";
import { expect, util, Assertion } from "chai";
import { setupRegistration } from "../lib/webauthn-registration.js";
import webauthn from "../lib/webauthn-core.js";
import { assert, fake, match, stub } from "sinon";

describe("webauthn-registration", () => {
  before(() => {
    Assertion.addProperty("visible", function () {
      const obj = util.flag(this, "object");
      new Assertion(obj).to.have.nested.property("style.display", "block");
    });
    Assertion.addProperty("hidden", function () {
      const obj = util.flag(this, "object");
      new Assertion(obj).to.have.nested.property("style.display", "none");
    });
  });

  describe("bootstrap", () => {
    let registerStub;
    let registerButton;
    let labelField;
    let errorPopup;
    let successPopup;
    let deleteForms;
    let ui;

    beforeEach(() => {
      registerStub = stub(webauthn, "register").resolves(undefined);
      errorPopup = {
        style: {
          display: undefined,
        },
        textContent: undefined,
      };
      successPopup = {
        style: {
          display: undefined,
        },
        textContent: undefined,
      };
      registerButton = {
        addEventListener: fake(),
      };
      labelField = {
        value: undefined,
      };
      deleteForms = [];
      ui = {
        getSuccess: function () {
          return successPopup;
        },
        getError: function () {
          return errorPopup;
        },
        getRegisterButton: function () {
          return registerButton;
        },
        getLabelInput: function () {
          return labelField;
        },
        getDeleteForms: function () {
          return deleteForms;
        },
      };
      global.window = {
        location: {
          href: {},
        },
      };
      global.console = {
        error: stub(),
      };
    });

    afterEach(() => {
      registerStub.restore();
      delete global.window;
    });

    describe("when webauthn is not supported", () => {
      beforeEach(() => {
        delete global.window.PublicKeyCredential;
      });

      it("does not set up a click event listener", async () => {
        await setupRegistration({}, "/", ui);

        assert.notCalled(registerButton.addEventListener);
      });

      it("shows an error popup", async () => {
        await setupRegistration({}, "/", ui);

        expect(errorPopup).to.be.visible;
        expect(errorPopup.textContent).to.equal("WebAuthn is not supported");
        expect(successPopup).to.be.hidden;
      });
    });

    describe("when webauthn is supported", () => {
      beforeEach(() => {
        global.window.PublicKeyCredential = fake();
      });

      it("hides the popups", async () => {
        await setupRegistration({}, "/", ui);

        expect(successPopup).to.be.hidden;
        expect(errorPopup).to.be.hidden;
      });

      it("sets up a click event listener on the register button", async () => {
        await setupRegistration({}, "/some/path", ui);

        assert.calledOnceWithMatch(registerButton.addEventListener, "click", match.typeOf("function"));
      });

      describe(`when the query string contains "success"`, () => {
        beforeEach(() => {
          global.window.location.search = "?success&continue=true";
        });

        it("shows the success popup", async () => {
          await setupRegistration({}, "/", ui);

          expect(successPopup).to.be.visible;
          expect(errorPopup).to.be.hidden;
        });
      });

      describe("when the register button is clicked", () => {
        const headers = { "x-header": "value" };
        const contextPath = "/some/path";

        beforeEach(async () => {
          await setupRegistration(headers, contextPath, ui);
        });

        it("hides all the popups", async () => {
          successPopup.textContent = "dummy-content";
          successPopup.style.display = "block";
          errorPopup.textContent = "dummy-content";
          errorPopup.style.display = "block";

          await registerButton.addEventListener.firstCall.lastArg();

          expect(successPopup).to.be.hidden;
          expect(errorPopup).to.be.hidden;
        });

        it("calls register", async () => {
          labelField.value = "passkey name";

          await registerButton.addEventListener.firstCall.lastArg();

          assert.calledOnceWithExactly(registerStub, headers, contextPath, labelField.value);
        });

        it("navigates to success page", async () => {
          labelField.value = "passkey name";

          await registerButton.addEventListener.firstCall.lastArg();

          expect(global.window.location.href).to.equal(`${contextPath}/webauthn/register?success`);
        });

        it("handles errors", async () => {
          registerStub.rejects(new Error("The registration failed"));

          await registerButton.addEventListener.firstCall.lastArg();

          expect(errorPopup.textContent).to.equal("The registration failed");
          expect(errorPopup).to.be.visible;
          expect(successPopup).to.be.hidden;
          assert.calledOnceWithMatch(
            global.console.error,
            match.instanceOf(Error).and(match.has("message", "The registration failed")),
          );
        });
      });

      describe("delete", () => {
        beforeEach(() => {
          global.fetch = fake.resolves({ ok: true });
        });

        afterEach(() => {
          delete global.fetch;
        });

        it("no errors when no forms", async () => {
          await setupRegistration({}, "/some/path", ui);
        });

        it("sets up forms for fetch", async () => {
          const deleteFormOne = {
            addEventListener: fake(),
          };
          const deleteFormTwo = {
            addEventListener: fake(),
          };
          deleteForms = [deleteFormOne, deleteFormTwo];

          await setupRegistration({}, "", ui);

          assert.calledOnceWithMatch(deleteFormOne.addEventListener, "submit", match.typeOf("function"));
          assert.calledOnceWithMatch(deleteFormTwo.addEventListener, "submit", match.typeOf("function"));
        });

        describe("when the delete button is clicked", () => {
          it("calls POST to the form action", async () => {
            const contextPath = "/some/path";
            const deleteForm = {
              addEventListener: fake(),
              action: `${contextPath}/webauthn/1234`,
            };
            deleteForms = [deleteForm];
            const headers = {
              "X-CSRF-TOKEN": "token",
            };

            await setupRegistration(headers, contextPath, ui);

            const clickEvent = {
              preventDefault: fake(),
            };
            await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
            assert.calledOnce(clickEvent.preventDefault);
            assert.calledOnceWithExactly(global.fetch, `/some/path/webauthn/1234`, {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                ...headers,
              },
            });
            expect(global.window.location.href).to.equal(`/some/path/webauthn/register?success`);
          });
        });

        it("handles errors", async () => {
          global.fetch = fake.rejects("Server threw an error");
          global.window.location.href = "/initial/location";
          const deleteForm = {
            addEventListener: fake(),
          };
          deleteForms = [deleteForm];

          await setupRegistration({}, "", ui);
          const clickEvent = { preventDefault: fake() };
          await deleteForm.addEventListener.firstCall.lastArg(clickEvent);

          expect(errorPopup).to.be.visible;
          expect(errorPopup.textContent).to.equal("Server threw an error");
          // URL does not change
          expect(global.window.location.href).to.equal("/initial/location");
        });
      });
    });
  });
});
