import { mock } from "jest-mock-extended";

import { FormPurposeCategories } from "@bitwarden/common/autofill/constants";

import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form";
import { createAutofillFieldMock, createAutofillFormMock } from "../spec/autofill-mocks";
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
import {
  ElementWithOpId,
  FillableFormFieldElement,
  FormFieldElement,
  FormElementWithAttribute,
} from "../types";
import * as autofillUtils from "../utils";

import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
import { CollectAutofillContentService } from "./collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
import { DomQueryService } from "./dom-query.service";

jest.mock("../utils", () => {
  const utils = jest.requireActual("../utils");
  return {
    ...utils,
    debounce: jest.fn((fn) => fn),
    // Call-through spy so scheduling tests can assert on it without changing behavior.
    requestIdleCallbackPolyfill: jest.fn((cb, opts) => utils.requestIdleCallbackPolyfill(cb, opts)),
  };
});

const mockLoginForm = `
  <div id="root">
    <form>
      <input type="text" id="username" />
      <input type="password" />
    </form>
  </div>
`;

const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdleCallback(resolve));

describe("CollectAutofillContentService", () => {
  const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
  const domElementVisibilityService = new DomElementVisibilityService();
  const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>();
  const domQueryService = new DomQueryService();
  const autofillOverlayContentService = new AutofillOverlayContentService(
    domQueryService,
    domElementVisibilityService,
    inlineMenuFieldQualificationService,
  );
  let collectAutofillContentService: CollectAutofillContentService;
  const mockIntersectionObserver = mock<IntersectionObserver>();

  beforeEach(() => {
    globalThis.requestIdleCallback = jest.fn((cb, options) => setTimeout(cb, 100));
    globalThis.cancelIdleCallback = jest.fn((id) => clearTimeout(id));
    document.body.innerHTML = mockLoginForm;
    collectAutofillContentService = new CollectAutofillContentService(
      domElementVisibilityService,
      domQueryService,
      autofillOverlayContentService,
    );
    window.IntersectionObserver = jest.fn(() => mockIntersectionObserver) as any;
  });

  afterEach(() => {
    jest.clearAllMocks();
    jest.restoreAllMocks();
    jest.clearAllTimers();
    document.body.innerHTML = "";
  });

  afterAll(() => {
    mockQuerySelectorAll.mockRestore();
  });

  describe("getPageDetails", () => {
    beforeEach(() => {
      jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: unknown) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({ result: null });
          }
        });
    });

    it("returns an object with empty forms and fields if no fields were found on a previous iteration", async () => {
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
      jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");

      await collectAutofillContentService.getPageDetails();

      expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalledWith({}, []);
      expect(
        collectAutofillContentService["queryAutofillFormAndFieldElements"],
      ).not.toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
    });

    it("returns an object with cached form and field data values", async () => {
      const formId = "validFormId";
      const formAction = "https://example.com/";
      const formMethod = "post";
      const formName = "validFormName";
      const usernameFieldId = "usernameField";
      const usernameFieldName = "username";
      const usernameFieldLabel = "User Name";
      const passwordFieldId = "passwordField";
      const passwordFieldName = "password";
      const passwordFieldLabel = "Password";
      document.body.innerHTML = `
        <form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
            <label for="${usernameFieldId}">${usernameFieldLabel}</label>
            <input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
            <label for="${passwordFieldId}">${passwordFieldLabel}</label>
            <input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
        </form>
      `;
      const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
      const autofillForm: AutofillForm = {
        opid: "__form__0",
        htmlAction: formAction,
        htmlName: formName,
        htmlID: formId,
        htmlMethod: formMethod,
        htmlClass: "",
        htmlAncestorHeadings: [],
      };
      const fieldElement = document.getElementById(
        usernameFieldId,
      ) as ElementWithOpId<FormFieldElement>;
      const autofillField: AutofillField = {
        opid: "__0",
        elementNumber: 0,
        maxLength: 999,
        viewable: true,
        htmlID: usernameFieldId,
        htmlName: usernameFieldName,
        htmlClass: null,
        tabindex: null,
        title: "",
        tagName: "input",
        "label-tag": usernameFieldLabel,
        "label-data": null,
        "label-aria": null,
        "label-top": null,
        "label-right": passwordFieldLabel,
        "label-left": usernameFieldLabel,
        placeholder: "",
        rel: null,
        type: "text",
        value: "",
        checked: false,
        autoCompleteType: null,
        disabled: false,
        readonly: false,
        selectInfo: null,
        form: "__form__0",
        "aria-hidden": false,
        "aria-disabled": false,
        "aria-haspopup": false,
        "data-stripe": null,
      };
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, autofillForm],
      ]);
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [fieldElement, autofillField],
      ]);
      jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
      jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFormsData");
      jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFieldsData");
      jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");

      await collectAutofillContentService.getPageDetails();

      expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalled();
      expect(collectAutofillContentService["getFormattedAutofillFormsData"]).toHaveBeenCalled();
      expect(collectAutofillContentService["getFormattedAutofillFieldsData"]).toHaveBeenCalled();
      expect(
        collectAutofillContentService["queryAutofillFormAndFieldElements"],
      ).not.toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
    });

    it("updates the visibility for cached autofill fields", async () => {
      const formId = "validFormId";
      const formAction = "https://example.com/";
      const formMethod = "post";
      const formName = "validFormName";
      const usernameFieldId = "usernameField";
      const usernameFieldName = "username";
      const usernameFieldLabel = "User Name";
      const passwordFieldId = "passwordField";
      const passwordFieldName = "password";
      const passwordFieldLabel = "Password";
      document.body.innerHTML = `
        <form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
            <label for="${usernameFieldId}">${usernameFieldLabel}</label>
            <input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
            <label for="${passwordFieldId}">${passwordFieldLabel}</label>
            <input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
        </form>
      `;
      const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
      const autofillForm: AutofillForm = {
        opid: "__form__0",
        htmlAction: formAction,
        htmlName: formName,
        htmlID: formId,
        htmlMethod: formMethod,
        htmlClass: "",
        htmlAncestorHeadings: [],
      };
      const fieldElement = document.getElementById(
        usernameFieldId,
      ) as ElementWithOpId<FormFieldElement>;
      const autofillField: AutofillField = {
        opid: "__0",
        elementNumber: 0,
        maxLength: 999,
        viewable: false,
        htmlID: usernameFieldId,
        htmlName: usernameFieldName,
        htmlClass: null,
        tabindex: null,
        title: "",
        tagName: "input",
        "label-tag": usernameFieldLabel,
        "label-data": null,
        "label-aria": null,
        "label-top": null,
        "label-right": passwordFieldLabel,
        "label-left": usernameFieldLabel,
        placeholder: "",
        rel: null,
        type: "text",
        value: "",
        checked: false,
        autoCompleteType: "",
        disabled: false,
        readonly: false,
        selectInfo: null,
        form: "__form__0",
        "aria-hidden": false,
        "aria-disabled": false,
        "aria-haspopup": false,
        "data-stripe": null,
      };
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, autofillForm],
      ]);
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [fieldElement, autofillField],
      ]);
      const isElementViewableSpy = jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);
      const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
        collectAutofillContentService["autofillOverlayContentService"],
        "setupOverlayListeners",
      );

      await collectAutofillContentService.getPageDetails();

      expect(autofillField.viewable).toBe(true);
      expect(isElementViewableSpy).toHaveBeenCalledWith(fieldElement);
      expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalled();
    });

    it("returns an object containing information about the current page as well as autofill data for the forms and fields of the page", async () => {
      const documentTitle = "Test Page";
      const formId = "validFormId";
      const formAction = "https://example.com/";
      const formMethod = "post";
      const formName = "validFormName";
      const usernameFieldId = "usernameField";
      const usernameFieldName = "username";
      const usernameFieldLabel = "User Name";
      const passwordFieldId = "passwordField";
      const passwordFieldName = "password";
      const passwordFieldLabel = "Password";
      document.title = documentTitle;
      document.body.innerHTML = `
        <form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
            <label for="${usernameFieldId}">${usernameFieldLabel}</label>
            <input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
            <label for="${passwordFieldId}">${passwordFieldLabel}</label>
            <input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
        </form>
      `;
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);

      const pageDetails = await collectAutofillContentService.getPageDetails();

      expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled();
      expect(pageDetails).toStrictEqual({
        title: documentTitle,
        url: window.location.href,
        documentUrl: document.location.href,
        forms: {
          __form__0: {
            opid: "__form__0",
            htmlAction: formAction,
            htmlClass: "",
            htmlName: formName,
            htmlID: formId,
            htmlMethod: formMethod,
            htmlAncestorHeadings: [],
          },
        },
        fields: [
          {
            opid: "__0",
            elementNumber: 0,
            maxLength: 999,
            viewable: true,
            htmlID: usernameFieldId,
            htmlName: usernameFieldName,
            htmlClass: null,
            tabindex: null,
            title: "",
            tagName: "input",
            "label-tag": usernameFieldLabel,
            "label-data": null,
            "label-aria": null,
            "label-top": null,
            "label-right": passwordFieldLabel,
            "label-left": usernameFieldLabel,
            placeholder: "",
            rel: null,
            type: "text",
            value: "",
            checked: false,
            autoCompleteType: null,
            disabled: false,
            readonly: false,
            selectInfo: null,
            form: "__form__0",
            "aria-hidden": false,
            "aria-describedby": null,
            "aria-disabled": false,
            "aria-haspopup": false,
            "data-stripe": null,
            dataSetValues: "",
          },
          {
            opid: "__1",
            elementNumber: 1,
            maxLength: 999,
            viewable: true,
            htmlID: passwordFieldId,
            htmlName: passwordFieldName,
            htmlClass: null,
            tabindex: null,
            title: "",
            tagName: "input",
            "label-tag": passwordFieldLabel,
            "label-data": null,
            "label-aria": null,
            "label-top": null,
            "label-right": "",
            "label-left": passwordFieldLabel,
            placeholder: "",
            rel: null,
            type: "password",
            value: "",
            checked: false,
            autoCompleteType: null,
            disabled: false,
            readonly: false,
            selectInfo: null,
            form: "__form__0",
            "aria-hidden": false,
            "aria-describedby": null,
            "aria-disabled": false,
            "aria-haspopup": false,
            "data-stripe": null,
            dataSetValues: "",
          },
        ],
        collectedTimestamp: expect.any(Number),
      });
    });

    it("sets the noFieldsFound property to true if the page has no forms or fields", async function () {
      document.body.innerHTML = "";
      collectAutofillContentService["noFieldsFound"] = false;
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");

      await collectAutofillContentService.getPageDetails();

      expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled();
      expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled();
      expect(collectAutofillContentService["noFieldsFound"]).toBe(true);
    });
  });

  describe("applyExternalTargetedFields", () => {
    it("registers matched elements in both autofillFieldElements and autofillFieldsByOpid", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;
      const targetedFields = [{ selector: "#username", fieldType: "username" }];

      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      const element = document.getElementById("username") as ElementWithOpId<FormFieldElement>;
      expect(collectAutofillContentService["autofillFieldElements"].has(element)).toBe(true);
      expect(
        collectAutofillContentService["autofillFieldsByOpid"].has("targeted_field_0_username"),
      ).toBe(true);
    });

    it("sends a collectPageDetailsResponse message after registering fields", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;
      const targetedFields = [{ selector: "#username", fieldType: "username" }];

      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
        expect.objectContaining({
          command: "collectPageDetailsResponse",
          sender: "autofillInit",
          details: expect.objectContaining({
            fields: expect.arrayContaining([expect.any(Object)]),
          }),
        }),
        expect.any(Function),
      );
    });

    it("does not send a collectPageDetailsResponse when no selectors match", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;
      const targetedFields = [{ selector: "#nonexistent", fieldType: "username" }];

      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith(
        expect.objectContaining({ command: "collectPageDetailsResponse" }),
        expect.any(Function),
      );
    });
  });

  describe("getTargetedPageDetails cached-field fallback", () => {
    beforeEach(() => {
      jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: string) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({
              result: [
                {
                  fields: {
                    username: ["iframe#nonexistent >>> #username"],
                  },
                },
              ],
            });
          }
          return Promise.resolve(undefined);
        });
    });

    it("returns empty page details when no local fields match and autofillFieldElements is empty", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;

      const pageDetails = await collectAutofillContentService.getPageDetails();

      expect(pageDetails.fields).toHaveLength(0);
    });

    it("returns cached page details from applyExternalTargetedFields when no local fields match", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;

      const targetedFields = [{ selector: "#username", fieldType: "username" }];
      jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: string) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({
              result: [{ fields: { username: ["iframe#nonexistent >>> #username"] } }],
            });
          }
          return Promise.resolve(undefined);
        });
      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      const pageDetails = await collectAutofillContentService.getPageDetails();

      expect(pageDetails.fields).toHaveLength(1);
      expect(pageDetails.fields[0].opid).toBe("targeted_field_0_username");
    });
  });

  describe("getTargetedPageDetails category threading", () => {
    it("carries the source form category onto locally-resolved targeted fields", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;
      jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: string) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({
              result: [
                {
                  category: FormPurposeCategories.AccountLogin,
                  fields: { email: ["#username"] },
                },
              ],
            });
          }
          return Promise.resolve(undefined);
        });

      const pageDetails = await collectAutofillContentService.getPageDetails();

      expect(pageDetails.fields).toHaveLength(1);
      expect(pageDetails.fields[0].fieldQualifier).toBe("email");
      expect(pageDetails.fields[0].formCategory).toBe(FormPurposeCategories.AccountLogin);
    });
  });

  describe("getTargetedPageDetails iframe routing", () => {
    const mockTargetingRules = (selector: string) =>
      jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: string) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({
              result: [{ fields: { username: [selector] } }],
            });
          }
          return Promise.resolve(undefined);
        });

    it("routes via routeTargetedFieldsToFrame using iframe.src when contentDocument is null (cross-origin)", async () => {
      document.body.innerHTML = `<iframe id="cross-iframe"></iframe>`;
      const iframe = document.getElementById("cross-iframe") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", {
        value: "https://other.example.com/login",
        configurable: true,
      });
      Object.defineProperty(iframe, "contentDocument", { value: null, configurable: true });

      const sendMessageSpy = mockTargetingRules("iframe#cross-iframe >>> #username");

      await collectAutofillContentService.getPageDetails();

      expect(sendMessageSpy).toHaveBeenCalledWith(
        "routeTargetedFieldsToFrame",
        expect.objectContaining({
          iframeSrc: "https://other.example.com/login",
          iframeTargetedFields: expect.arrayContaining([
            expect.objectContaining({ fieldType: "username" }),
          ]),
        }),
      );
    });

    it("does not route when iframe.src is empty (srcdoc / about:blank fail-soft)", async () => {
      document.body.innerHTML = `<iframe id="srcdoc-iframe"></iframe>`;
      const iframe = document.getElementById("srcdoc-iframe") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", { value: "", configurable: true });
      Object.defineProperty(iframe, "contentDocument", { value: null, configurable: true });

      const sendMessageSpy = mockTargetingRules("iframe#srcdoc-iframe >>> #username");

      await collectAutofillContentService.getPageDetails();

      expect(sendMessageSpy).not.toHaveBeenCalledWith(
        "routeTargetedFieldsToFrame",
        expect.anything(),
      );
    });

    it("prefers contentDocument.location.href over iframe.src when both are available", async () => {
      document.body.innerHTML = `<iframe id="same-iframe"></iframe>`;
      const iframe = document.getElementById("same-iframe") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", {
        value: "https://stale.example.com/page",
        configurable: true,
      });
      Object.defineProperty(iframe, "contentDocument", {
        value: { location: { href: "https://current.example.com/page" } },
        configurable: true,
      });

      const sendMessageSpy = mockTargetingRules("iframe#same-iframe >>> #username");

      await collectAutofillContentService.getPageDetails();

      expect(sendMessageSpy).toHaveBeenCalledWith(
        "routeTargetedFieldsToFrame",
        expect.objectContaining({
          iframeSrc: "https://current.example.com/page",
        }),
      );
    });

    it("includes the source form category in the routed iframe payload", async () => {
      document.body.innerHTML = `<iframe id="login-form-container"></iframe>`;
      const iframe = document.getElementById("login-form-container") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", {
        value: "https://other.example.com/login",
        configurable: true,
      });
      Object.defineProperty(iframe, "contentDocument", { value: null, configurable: true });

      const sendMessageSpy = jest
        .spyOn(collectAutofillContentService as any, "sendExtensionMessage")
        .mockImplementation((command: string) => {
          if (command === "getUrlAutofillTargetingRules") {
            return Promise.resolve({
              result: [
                {
                  category: FormPurposeCategories.AccountLogin,
                  fields: { username: ["iframe#login-form-container >>> #username"] },
                },
              ],
            });
          }
          return Promise.resolve(undefined);
        });

      await collectAutofillContentService.getPageDetails();

      expect(sendMessageSpy).toHaveBeenCalledWith(
        "routeTargetedFieldsToFrame",
        expect.objectContaining({
          iframeTargetedFields: expect.arrayContaining([
            expect.objectContaining({
              fieldType: "username",
              formCategory: FormPurposeCategories.AccountLogin,
            }),
          ]),
        }),
      );
    });
  });

  describe("applyExternalTargetedFields recursion", () => {
    it("re-routes via routeTargetedFieldsToFrame when received selector itself crosses another iframe", async () => {
      document.body.innerHTML = `<iframe id="inner-iframe"></iframe>`;
      const iframe = document.getElementById("inner-iframe") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", {
        value: "https://leaf.example.com/login",
        configurable: true,
      });
      Object.defineProperty(iframe, "contentDocument", { value: null, configurable: true });

      const sendMessageSpy = jest.spyOn(
        collectAutofillContentService as any,
        "sendExtensionMessage",
      );

      const targetedFields = [
        { selector: "iframe#inner-iframe >>> #username", fieldType: "username" },
      ];

      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      expect(sendMessageSpy).toHaveBeenCalledWith(
        "routeTargetedFieldsToFrame",
        expect.objectContaining({
          iframeSrc: "https://leaf.example.com/login",
          iframeTargetedFields: expect.arrayContaining([
            expect.objectContaining({ fieldType: "username" }),
          ]),
        }),
      );
    });

    it("retains the routed form category on a locally-resolved field", async () => {
      document.body.innerHTML = `<input type="text" id="username" />`;

      await collectAutofillContentService.applyExternalTargetedFields([
        {
          selector: "#username",
          fieldType: "username",
          formCategory: FormPurposeCategories.AccountLogin,
        },
      ]);

      const fields = Array.from(
        (
          collectAutofillContentService as any
        ).autofillFieldElements.values() as Iterable<AutofillField>,
      );
      expect(fields).toHaveLength(1);
      expect(fields[0].formCategory).toBe(FormPurposeCategories.AccountLogin);
    });

    it("does not send collectPageDetailsResponse when all selectors route onward and no fields are cached", async () => {
      document.body.innerHTML = `<iframe id="inner-iframe"></iframe>`;
      const iframe = document.getElementById("inner-iframe") as HTMLIFrameElement;
      Object.defineProperty(iframe, "src", {
        value: "https://leaf.example.com/login",
        configurable: true,
      });
      Object.defineProperty(iframe, "contentDocument", { value: null, configurable: true });

      const targetedFields = [
        { selector: "iframe#inner-iframe >>> #username", fieldType: "username" },
      ];

      await collectAutofillContentService.applyExternalTargetedFields(targetedFields);

      expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith(
        expect.objectContaining({ command: "collectPageDetailsResponse" }),
        expect.any(Function),
      );
    });
  });

  describe("getAutofillFieldElementByOpid", () => {
    it("returns the element with the opid property value matching the passed value", () => {
      const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute;
      const passwordInput = document.querySelector(
        'input[type="password"]',
      ) as FormElementWithAttribute;
      textInput.opid = "__0";
      passwordInput.opid = "__1";

      const textInputWithOpid = collectAutofillContentService.getAutofillFieldElementByOpid("__0");
      const passwordInputWithOpid =
        collectAutofillContentService.getAutofillFieldElementByOpid("__1");

      expect(textInputWithOpid).toEqual(textInput);
      expect(textInputWithOpid).not.toEqual(passwordInput);
      expect(passwordInputWithOpid).toEqual(passwordInput);
    });

    it("returns the first of the element with an `opid` value matching the passed value and emits a console warning if multiple fields contain the same `opid`", () => {
      const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute;
      const passwordInput = document.querySelector(
        'input[type="password"]',
      ) as FormElementWithAttribute;
      jest.spyOn(console, "warn").mockImplementationOnce(jest.fn());
      textInput.opid = "__1";
      passwordInput.opid = "__1";

      const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0");
      const elementWithOpid1 = collectAutofillContentService.getAutofillFieldElementByOpid("__1");

      expect(elementWithOpid0).toEqual(textInput);
      expect(elementWithOpid1).toEqual(textInput);
      expect(elementWithOpid1).not.toEqual(passwordInput);
      // eslint-disable-next-line no-console
      expect(console.warn).toHaveBeenCalledWith("More than one element found with opid __1");
    });

    it("returns the element at the index position (parsed from passed opid) of all AutofillField elements when the passed opid value cannot be found", () => {
      const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute;
      const passwordInput = document.querySelector(
        'input[type="password"]',
      ) as FormElementWithAttribute;
      textInput.opid = undefined;
      passwordInput.opid = "__1";

      const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0");
      const elementWithOpid2 = collectAutofillContentService.getAutofillFieldElementByOpid("__2");

      expect(textInput.opid).toBeUndefined();
      expect(elementWithOpid0).toEqual(textInput);
      expect(elementWithOpid0).not.toEqual(passwordInput);
      expect(elementWithOpid2).toBeNull();
    });

    it("returns null if no element can be found", () => {
      const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute;
      textInput.opid = "__0";

      const foundElementWithOpid =
        collectAutofillContentService.getAutofillFieldElementByOpid("__999");

      expect(foundElementWithOpid).toBeNull();
    });
  });

  describe("buildAutofillFormsData", () => {
    it("will not attempt to gather data from a cached form element", () => {
      const documentTitle = "Test Page";
      const formId = "validFormId";
      const formAction = "https://example.com/";
      const formMethod = "post";
      const formName = "validFormName";
      document.title = documentTitle;
      document.body.innerHTML = `
        <form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
            <label for="usernameFieldId">usernameFieldLabel</label>
            <input type="text" id="usernameFieldId" name="usernameFieldName" />
            <label for="passwordFieldId">passwordFieldLabel</label>
            <input type="password" id="passwordFieldId" name="passwordFieldName" />
        </form>

      `;
      const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
      const existingAutofillForm: AutofillForm = {
        opid: "__form__0",
        htmlAction: formAction,
        htmlName: formName,
        htmlID: formId,
        htmlMethod: formMethod,
        htmlClass: "",
        htmlAncestorHeadings: [],
      };
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, existingAutofillForm],
      ]);
      const formElements = Array.from(document.querySelectorAll("form"));
      jest.spyOn(collectAutofillContentService as any, "getFormActionAttribute");

      const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"](
        formElements as Node[],
      );

      expect(collectAutofillContentService["getFormActionAttribute"]).not.toHaveBeenCalled();
      expect(autofillFormsData).toStrictEqual({ __form__0: existingAutofillForm });
    });

    it("returns an object of AutofillForm objects with the form id as a key", () => {
      const documentTitle = "Test Page";
      const formId1 = "validFormId";
      const formAction1 = "https://example.com/";
      const formMethod1 = "post";
      const formName1 = "validFormName";
      const formId2 = "validFormId2";
      const formAction2 = "https://example2.com/";
      const formMethod2 = "get";
      const formName2 = "validFormName2";
      document.title = documentTitle;
      document.body.innerHTML = `
        <form id="${formId1}" action="${formAction1}" method="${formMethod1}" name="${formName1}">
            <label for="usernameFieldId">usernameFieldLabel</label>
            <input type="text" id="usernameFieldId" name="usernameFieldName" />
            <label for="passwordFieldId">passwordFieldLabel</label>
            <input type="password" id="passwordFieldId" name="passwordFieldName" />
        </form>
        <form id="${formId2}" action="${formAction2}" method="${formMethod2}" name="${formName2}">
            <label for="searchField">searchFieldLabel</label>
            <input type="search" id="searchField" name="searchFieldName" />
        </form>
      `;

      const { formElements } = collectAutofillContentService["queryAutofillFormAndFieldElements"]();
      const autofillFormsData =
        collectAutofillContentService["buildAutofillFormsData"](formElements);

      expect(autofillFormsData).toStrictEqual({
        __form__0: {
          opid: "__form__0",
          htmlAction: formAction1,
          htmlClass: "",
          htmlName: formName1,
          htmlID: formId1,
          htmlMethod: formMethod1,
          htmlAncestorHeadings: [],
        },
        __form__1: {
          opid: "__form__1",
          htmlAction: formAction2,
          htmlClass: "",
          htmlName: formName2,
          htmlID: formId2,
          htmlMethod: formMethod2,
          htmlAncestorHeadings: [],
        },
      });
    });
  });

  describe("getAncestorHeadings", () => {
    it("returns an empty array when the form has no parent element", () => {
      const orphanForm = document.createElement("form");

      const result = collectAutofillContentService["getAncestorHeadings"](orphanForm);

      expect(result).toEqual([]);
    });

    it("returns an empty array when no semantic-section ancestor encloses the form", () => {
      document.body.innerHTML = `
        <div><div><form id="f"><input type="email" /></form></div></div>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual([]);
    });

    it("captures a single heading inside the form's nearest semantic-section ancestor (raw casing preserved)", () => {
      document.body.innerHTML = `
        <section>
          <h2>Subscribe to our newsletter</h2>
          <form id="f"><input type="email" /></form>
        </section>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual(["Subscribe to our newsletter"]);
    });

    it("captures a heading nested several wrapper ancestors above the form when a semantic section encloses both", () => {
      document.body.innerHTML = `
        <section>
          <div>
            <div>
              <header>
                <div>
                  <h3>
                    <span>Subscribe to our newsletter</span>
                  </h3>
                </div>
              </header>
              <div>
                <div>
                  <form id="f"><input type="email" /></form>
                </div>
              </div>
            </div>
          </div>
        </section>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toContain("Subscribe to our newsletter");
    });

    it("does not include headings that belong to a different sibling form within the same scope", () => {
      document.body.innerHTML = `
        <section>
          <h2>Other form heading</h2>
          <form id="other"><h3>Inside other form</h3><input type="text" /></form>
          <form id="target"><input type="email" /></form>
        </section>
      `;
      const formElement = document.getElementById("target") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual(["Other form heading"]);
    });

    it("returns each heading as its own entry so keyword scans cannot match across boundaries", () => {
      document.body.innerHTML = `
        <section>
          <h2>Newsletter</h2>
          <h3>Sign-up</h3>
          <form id="f"><input type="email" /></form>
        </section>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual(["Newsletter", "Sign-up"]);
    });

    it("scopes to the nearest semantic-section ancestor and excludes headings outside it", () => {
      document.body.innerHTML = `
        <h1>Page Title (outside the section)</h1>
        <section>
          <h2>Section Heading</h2>
          <div><div><form id="f"><input type="email" /></form></div></div>
        </section>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual(["Section Heading"]);
    });

    it("orders headings by depth of common ancestor with the form, closest first", () => {
      document.body.innerHTML = `
        <article>
          <h1>Article Heading</h1>
          <div>
            <h2>Container Heading</h2>
            <div>
              <h3>Form Heading</h3>
              <form id="f"><input type="email" /></form>
            </div>
          </div>
        </article>
      `;
      const formElement = document.getElementById("f") as HTMLFormElement;

      const result = collectAutofillContentService["getAncestorHeadings"](formElement);

      expect(result).toEqual(["Form Heading", "Container Heading", "Article Heading"]);
    });
  });

  describe("buildAutofillFieldsData", () => {
    it("returns a promise containing an array of AutofillField objects", async () => {
      jest.spyOn(collectAutofillContentService as any, "getAutofillFieldElements");
      jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);

      const { formFieldElements } =
        collectAutofillContentService["queryAutofillFormAndFieldElements"]();
      const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"](
        formFieldElements as FormFieldElement[],
      );
      const autofillFieldsData = await Promise.resolve(autofillFieldsPromise);

      expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(
        200,
        formFieldElements,
      );
      expect(collectAutofillContentService["buildAutofillFieldItem"]).toHaveBeenCalledTimes(2);
      expect(autofillFieldsPromise).toBeInstanceOf(Promise);
      expect(autofillFieldsData).toStrictEqual([
        {
          "aria-describedby": null,
          "aria-disabled": false,
          "aria-haspopup": false,
          "aria-hidden": false,
          autoCompleteType: null,
          checked: false,
          "data-stripe": null,
          disabled: false,
          elementNumber: 0,
          form: null,
          htmlClass: null,
          htmlID: "username",
          htmlName: "",
          "label-aria": null,
          "label-data": null,
          "label-left": "",
          "label-right": "",
          "label-tag": "",
          "label-top": null,
          maxLength: 999,
          opid: "__0",
          placeholder: "",
          readonly: false,
          rel: null,
          selectInfo: null,
          tabindex: null,
          tagName: "input",
          title: "",
          type: "text",
          value: "",
          viewable: true,
          dataSetValues: "",
        },
        {
          "aria-describedby": null,
          "aria-disabled": false,
          "aria-haspopup": false,
          "aria-hidden": false,
          autoCompleteType: null,
          checked: false,
          "data-stripe": null,
          disabled: false,
          elementNumber: 1,
          form: null,
          htmlClass: null,
          htmlID: "",
          htmlName: "",
          "label-aria": null,
          "label-data": null,
          "label-left": "",
          "label-right": "",
          "label-tag": "",
          "label-top": null,
          maxLength: 999,
          opid: "__1",
          placeholder: "",
          readonly: false,
          rel: null,
          selectInfo: null,
          tabindex: null,
          tagName: "input",
          title: "",
          type: "password",
          value: "",
          viewable: true,
          dataSetValues: "",
        },
      ]);
    });
  });

  describe("getAutofillFieldElements", () => {
    it("returns all form elements from the targeted document if no limit is set", () => {
      document.body.innerHTML = `
      <div id="root">
        <form>
          <label for="username">Username</label>
          <input type="text" id="username" />
          <label for="password">Password</label>
          <input type="password" />
          <label for="comments">Comments</label>
          <textarea id="comments"></textarea>
          <label for="select">Select</label>
          <select id="select">
            <option value="1">1</option>
            <option value="2">2</option>
          </select>
          <span data-bwautofill="true">Span Element</span>
        </form>
      </div>
      `;
      const usernameInput = document.getElementById("username");
      const passwordInput = document.querySelector('input[type="password"]');
      const commentsTextarea = document.getElementById("comments");
      const selectElement = document.getElementById("select");
      const spanElement = document.querySelector('span[data-bwautofill="true"]');
      jest.spyOn(document, "querySelectorAll");
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");

      const formElements: FormFieldElement[] =
        collectAutofillContentService["getAutofillFieldElements"]();

      expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
      expect(formElements).toEqual([
        usernameInput,
        passwordInput,
        commentsTextarea,
        selectElement,
        spanElement,
      ]);
    });

    it("returns up to 2 (passed as `limit`) form elements from the targeted document with more than 2 form elements", () => {
      document.body.innerHTML = `
        <div>
          <span data-bwautofill="true">included span</span>
          <textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
          <span>ignored span</span>
          <select><option value="1">Option 1</option></select>
          <label for="username">username</label>
          <input type="text" id="username" />
          <input type="password" />
          <span data-bwautofill="true">another included span</span>
        </div>
      `;
      const spanElement = document.querySelector("span[data-bwautofill='true']");
      const textAreaInput = document.querySelector("textarea");
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");

      const formElements: FormFieldElement[] =
        collectAutofillContentService["getAutofillFieldElements"](2);

      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        1,
        spanElement,
        "type",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        2,
        textAreaInput,
        "type",
      );
      expect(formElements).toEqual([spanElement, textAreaInput]);
    });

    it("returns form elements from the targeted document, ignoring input types `hidden`, `submit`, `reset`, `button`, `image`, `file`, and inputs tagged with `data-bwignore`, while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit", () => {
      document.body.innerHTML = `
        <div>
          <fieldset>
            <legend>Select an option:</legend>
            <div>
              <input type="radio" value="option-a" />
              <label for="option-a">Option A: Options B & C</label>
            </div>
            <div>
              <input type="radio" value="option-b" />
              <label for="option-b">Option B: Options A & C</label>
            </div>
            <div>
              <input type="radio" value="option-c" />
              <label for="option-c">Option C: Options A & B</label>
            </div>
          </fieldset>
          <span data-bwautofill="true" id="first-span">included span</span>
          <textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
          <span>ignored span</span>
          <input type="checkbox" name="doYouWantToCheck" />
          <label for="doYouWantToCheck">Do you want to skip checking this box?</label>
          <select><option value="1">Option 1</option></select>
          <label for="username">username</label>
          <input type="text" data-bwignore value="None" />
          <input type="hidden" value="of" />
          <input type="submit" value="these" />
          <input type="reset" value="inputs" />
          <input type="button" value="should" />
          <input type="image" src="be" />
          <input type="file" multiple id="returned" />
          <input type="text" id="username" />
          <input type="password" />
          <span data-bwautofill="true" id="second-span">another included span</span>
        </div>
      `;
      const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]');
      const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]');
      const inputRadioC = document.querySelector('input[type="radio"][value="option-c"]');
      const firstSpan = document.getElementById("first-span");
      const textAreaInput = document.querySelector("textarea");
      const checkboxInput = document.querySelector('input[type="checkbox"]');
      const selectElement = document.querySelector("select");
      const usernameInput = document.getElementById("username");
      const passwordInput = document.querySelector('input[type="password"]');
      const secondSpan = document.getElementById("second-span");

      const formElements: FormFieldElement[] =
        collectAutofillContentService["getAutofillFieldElements"]();

      expect(formElements).toEqual([
        inputRadioA,
        inputRadioB,
        inputRadioC,
        firstSpan,
        textAreaInput,
        checkboxInput,
        selectElement,
        usernameInput,
        passwordInput,
        secondSpan,
      ]);
    });

    it("returns form elements from the targeted document while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit`", () => {
      document.body.innerHTML = `
        <div>
          <input type="checkbox" name="doYouWantToCheck" />
          <label for="doYouWantToCheck">Do you want to skip checking this box?</label>
          <textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
          <span>ignored span</span>
          <fieldset>
            <legend>Select an option:</legend>
            <div>
              <input type="radio" value="option-a" />
              <label for="option-a">Option A: Options B & C</label>
            </div>
            <div>
              <input type="radio" value="option-b" />
              <label for="option-b">Option B: Options A & C</label>
            </div>
            <div>
              <input type="radio" value="option-c" />
              <label for="option-c">Option C: Options A & B</label>
            </div>
          </fieldset>
          <select><option value="1">Option 1</option></select>
          <label for="username">username</label>
          <input type="text" id="username" />
          <input type="password" />
          <span data-bwautofill="true">another included span</span>
        </div>
      `;
      const textAreaInput = document.querySelector("textarea");
      const selectElement = document.querySelector("select");
      const usernameInput = document.getElementById("username");
      const passwordInput = document.querySelector('input[type="password"]');
      const includedSpan = document.querySelector('span[data-bwautofill="true"]');
      const checkboxInput = document.querySelector('input[type="checkbox"]');
      const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]');
      const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]');

      const truncatedFormElements: FormFieldElement[] =
        collectAutofillContentService["getAutofillFieldElements"](8);

      expect(truncatedFormElements).toEqual([
        textAreaInput,
        selectElement,
        usernameInput,
        passwordInput,
        includedSpan,
        checkboxInput,
        inputRadioA,
        inputRadioB,
      ]);
    });
  });

  describe("buildAutofillFieldItem", () => {
    it("returns a `null` value if the field is a child of a `button[type='submit']`", async () => {
      const usernameField = {
        labelText: "Username",
        id: "username-id",
        type: "text",
      };
      document.body.innerHTML = `
        <form>
          <div>
            <div>
              <label for="${usernameField.id}">${usernameField.labelText}</label>
              <button type="submit">
                <input id="${usernameField.id}" type="${usernameField.type}" />
              </button>
            </div>
          </div>
        </form>
      `;
      const usernameInput = document.getElementById(
        usernameField.id,
      ) as ElementWithOpId<FillableFormFieldElement>;

      const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
        usernameInput,
        0,
      );

      expect(autofillFieldItem).toBeNull();
    });

    it("returns an existing autofill field item if it exists", async () => {
      const index = 0;
      const usernameField = {
        labelText: "Username",
        id: "username-id",
        classes: "username input classes",
        name: "username",
        type: "text",
        maxLength: 42,
        tabIndex: 0,
        title: "Username Input Title",
        autocomplete: "username-autocomplete",
        dataLabel: "username-data-label",
        ariaLabel: "username-aria-label",
        placeholder: "username-placeholder",
        rel: "username-rel",
        value: "username-value",
        dataStripe: "data-stripe",
      };
      document.body.innerHTML = `
        <form>
          <label for="${usernameField.id}">${usernameField.labelText}</label>
          <input
            id="${usernameField.id}"
            class="${usernameField.classes}"
            name="${usernameField.name}"
            type="${usernameField.type}"
            maxlength="${usernameField.maxLength}"
            tabindex="${usernameField.tabIndex}"
            title="${usernameField.title}"
            autocomplete="${usernameField.autocomplete}"
            data-label="${usernameField.dataLabel}"
            aria-label="${usernameField.ariaLabel}"
            placeholder="${usernameField.placeholder}"
            rel="${usernameField.rel}"
            value="${usernameField.value}"
            data-stripe="${usernameField.dataStripe}"
          />
        </form>
      `;
      const existingFieldData: AutofillField = {
        elementNumber: index,
        htmlClass: usernameField.classes,
        htmlID: usernameField.id,
        htmlName: usernameField.name,
        maxLength: usernameField.maxLength,
        opid: `__${index}`,
        tabindex: String(usernameField.tabIndex),
        tagName: "input",
        title: usernameField.title,
        viewable: true,
      };
      const usernameInput = document.getElementById(
        usernameField.id,
      ) as ElementWithOpId<FillableFormFieldElement>;
      usernameInput.opid = "__0";
      collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData);
      jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
      jest.spyOn(collectAutofillContentService as any, "getElementValue");

      const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
        usernameInput,
        0,
      );

      expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled();
      expect(
        collectAutofillContentService["domElementVisibilityService"].isElementViewable,
      ).not.toHaveBeenCalled();
      expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
      expect(autofillFieldItem).toEqual(existingFieldData);
    });

    it("returns the AutofillField base data values without the field labels or input values if the passed element is a span element", async () => {
      const index = 0;
      const spanElementId = "span-element";
      const spanElementClasses = "span element classes";
      const spanElementTabIndex = 0;
      const spanElementTitle = "Span Element Title";
      document.body.innerHTML = `
        <span id="${spanElementId}" class="${spanElementClasses}" tabindex="${spanElementTabIndex}" title="${spanElementTitle}">Span Element</span>
      `;
      const spanElement = document.getElementById(
        spanElementId,
      ) as ElementWithOpId<FormFieldElement>;
      jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
      jest.spyOn(collectAutofillContentService as any, "getElementValue");

      const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
        spanElement,
        index,
      );

      expect(collectAutofillContentService["getAutofillFieldMaxLength"]).toHaveBeenCalledWith(
        spanElement,
      );
      expect(
        collectAutofillContentService["domElementVisibilityService"].isElementViewable,
      ).toHaveBeenCalledWith(spanElement);
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        1,
        spanElement,
        "id",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        2,
        spanElement,
        "name",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        3,
        spanElement,
        "class",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        4,
        spanElement,
        "tabindex",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        5,
        spanElement,
        "title",
      );
      expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
        6,
        spanElement,
        "tagName",
      );
      expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
      expect(autofillFieldItem).toEqual({
        elementNumber: index,
        htmlClass: spanElementClasses,
        htmlID: spanElementId,
        htmlName: null,
        maxLength: null,
        opid: `__${index}`,
        tabindex: String(spanElementTabIndex),
        tagName: spanElement.tagName.toLowerCase(),
        title: spanElementTitle,
        viewable: true,
        dataSetValues: "",
      });
    });

    it("returns the AutofillField base data, label data, and input element data", async () => {
      const index = 0;
      const usernameField = {
        labelText: "Username",
        id: "username-id",
        classes: "username input classes",
        name: "username",
        type: "text",
        maxLength: 42,
        tabIndex: 0,
        title: "Username Input Title",
        autocomplete: "username-autocomplete",
        dataLabel: "username-data-label",
        ariaLabel: "username-aria-label",
        placeholder: "username-placeholder",
        rel: "username-rel",
        value: "username-value",
        dataStripe: "data-stripe",
      };
      document.body.innerHTML = `
        <form>
          <label for="${usernameField.id}">${usernameField.labelText}</label>
          <input
            id="${usernameField.id}"
            class="${usernameField.classes}"
            name="${usernameField.name}"
            type="${usernameField.type}"
            maxlength="${usernameField.maxLength}"
            tabindex="${usernameField.tabIndex}"
            title="${usernameField.title}"
            autocomplete="${usernameField.autocomplete}"
            data-label="${usernameField.dataLabel}"
            aria-label="${usernameField.ariaLabel}"
            placeholder="${usernameField.placeholder}"
            rel="${usernameField.rel}"
            value="${usernameField.value}"
            data-stripe="${usernameField.dataStripe}"
          />
        </form>
      `;
      const formElement = document.querySelector("form");
      formElement.opid = "form-opid";
      const usernameInput = document.getElementById(
        usernameField.id,
      ) as ElementWithOpId<FillableFormFieldElement>;
      jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
      jest.spyOn(collectAutofillContentService as any, "getElementValue");

      const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
        usernameInput,
        index,
      );

      expect(autofillFieldItem).toEqual({
        "aria-describedby": null,
        "aria-disabled": false,
        "aria-haspopup": false,
        "aria-hidden": false,
        autoCompleteType: usernameField.autocomplete,
        checked: false,
        "data-stripe": usernameField.dataStripe,
        disabled: false,
        elementNumber: index,
        form: formElement.opid,
        htmlClass: usernameField.classes,
        htmlID: usernameField.id,
        htmlName: usernameField.name,
        "label-aria": usernameField.ariaLabel,
        "label-data": usernameField.dataLabel,
        "label-left": usernameField.labelText,
        "label-right": "",
        "label-tag": usernameField.labelText,
        "label-top": null,
        maxLength: usernameField.maxLength,
        opid: `__${index}`,
        placeholder: usernameField.placeholder,
        readonly: false,
        rel: usernameField.rel,
        selectInfo: null,
        tabindex: String(usernameField.tabIndex),
        tagName: usernameInput.tagName.toLowerCase(),
        title: usernameField.title,
        type: usernameField.type,
        value: usernameField.value,
        viewable: true,
        dataSetValues: "label: username-data-label, stripe: data-stripe, ",
      });
    });

    it("returns the AutofillField base data and input element data, but not the label data if the input element is of type `hidden`", async () => {
      const index = 0;
      const hiddenField = {
        labelText: "Hidden Field",
        id: "hidden-id",
        classes: "hidden input classes",
        name: "hidden",
        type: "hidden",
        maxLength: 42,
        tabIndex: 0,
        title: "Hidden Input Title",
        autocomplete: "off",
        rel: "hidden-rel",
        value: "hidden-value",
        dataStripe: "data-stripe",
      };
      document.body.innerHTML = `
        <form>
          <label for="${hiddenField.id}">${hiddenField.labelText}</label>
          <input
            id="${hiddenField.id}"
            class="${hiddenField.classes}"
            name="${hiddenField.name}"
            type="${hiddenField.type}"
            maxlength="${hiddenField.maxLength}"
            tabindex="${hiddenField.tabIndex}"
            title="${hiddenField.title}"
            autocomplete="${hiddenField.autocomplete}"
            rel="${hiddenField.rel}"
            value="${hiddenField.value}"
            data-stripe="${hiddenField.dataStripe}"
          />
        </form>
      `;
      const formElement = document.querySelector("form");
      formElement.opid = "form-opid";
      const hiddenInput = document.getElementById(
        hiddenField.id,
      ) as ElementWithOpId<FillableFormFieldElement>;
      jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
      jest
        .spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
        .mockResolvedValue(true);
      jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
      jest.spyOn(collectAutofillContentService as any, "getElementValue");

      const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
        hiddenInput,
        index,
      );

      expect(autofillFieldItem).toEqual({
        "aria-describedby": null,
        "aria-disabled": false,
        "aria-haspopup": false,
        "aria-hidden": false,
        autoCompleteType: "off",
        checked: false,
        "data-stripe": hiddenField.dataStripe,
        disabled: false,
        elementNumber: index,
        form: formElement.opid,
        htmlClass: hiddenField.classes,
        htmlID: hiddenField.id,
        htmlName: hiddenField.name,
        maxLength: hiddenField.maxLength,
        opid: `__${index}`,
        readonly: false,
        rel: hiddenField.rel,
        selectInfo: null,
        tabindex: String(hiddenField.tabIndex),
        tagName: hiddenInput.tagName.toLowerCase(),
        title: hiddenField.title,
        type: hiddenField.type,
        value: hiddenField.value,
        viewable: true,
        dataSetValues: "stripe: data-stripe, ",
      });
    });
  });

  describe("createAutofillFieldLabelTag", () => {
    beforeEach(() => {
      jest.spyOn(collectAutofillContentService as any, "createLabelElementsTag");
      jest.spyOn(document, "querySelectorAll");
    });

    it("returns the label tag early if the passed element contains any labels", () => {
      document.body.innerHTML = `
        <label for="username-id">Username</label>
        <input type="text" id="username-id" name="username" />

      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set(element.labels),
      );
      expect(document.querySelectorAll).not.toHaveBeenCalled();
      expect(labelTag).toEqual("Username");
    });

    it("queries all labels associated with the element's id", () => {
      document.body.innerHTML = `
        <label for="country-id">Country</label>
        <span id="country-id"></span>
      `;
      const element = document.querySelector("#country-id") as FillableFormFieldElement;
      const elementLabel = document.querySelector("label[for='country-id']");

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.id}"]`);
      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set([elementLabel]),
      );
      expect(labelTag).toEqual("Country");
    });

    it("queries all labels associated with the element's name", () => {
      document.body.innerHTML = `
        <label for="country-name">Country</label>
        <select name="country-name"></select>
      `;
      const element = document.querySelector("select") as FillableFormFieldElement;
      const elementLabel = document.querySelector("label[for='country-name']");

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(document.querySelectorAll).not.toHaveBeenCalledWith(`label[for="${element.id}"]`);
      expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.name}"]`);
      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set([elementLabel]),
      );
      expect(labelTag).toEqual("Country");
    });

    it("will not add duplicate labels that are found to the label tag", () => {
      document.body.innerHTML = `
        <label for="country-name">Country</label>
        <div id="country-name" name="country-name"></div>
      `;
      const element = document.querySelector("#country-name") as FillableFormFieldElement;
      element.name = "country-name";
      const elementLabel = document.querySelector("label[for='country-name']");

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(document.querySelectorAll).toHaveBeenCalledWith(
        `label[for="${element.id}"], label[for="${element.name}"]`,
      );
      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set([elementLabel]),
      );
      expect(labelTag).toEqual("Country");
    });

    it("will attempt to identify the label of an element from its parent element", () => {
      document.body.innerHTML = `<label>
        Username
        <input type="text" id="username-id">
      </label>`;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;
      const elementLabel = element.parentElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set([elementLabel]),
      );
      expect(labelTag).toEqual("Username");
    });

    it("will attempt to identify the label of an element from a `dt` element associated with the element's parent", () => {
      document.body.innerHTML = `
        <dl>
          <dt id="label-element">Username</dt>
          <dd>
            <input type="text" id="username-id">
          </dd>
        </dl>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;
      const elementLabel = document.querySelector("#label-element");

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith(
        new Set([elementLabel]),
      );
      expect(labelTag).toEqual("Username");
    });

    it("will return an empty string value if no labels can be found for an element", () => {
      document.body.innerHTML = `
        <input type="text" id="username-id">
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element);

      expect(labelTag).toEqual("");
    });
  });

  describe("queryElementLabels", () => {
    it("returns null if the passed element has no id or name", () => {
      document.body.innerHTML = `
        <label for="username-id">
          Username
          <input type="text">
        </label>
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labels = collectAutofillContentService["queryElementLabels"](element);

      expect(labels).toBeNull();
    });

    it("returns an empty NodeList if the passed element has no label", () => {
      document.body.innerHTML = `
        <input type="text" id="username-id">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labels = collectAutofillContentService["queryElementLabels"](element);

      expect(labels).toEqual(document.querySelectorAll("label"));
    });

    it("returns the label of an element associated with its ID value", () => {
      document.body.innerHTML = `
        <label for="username-id">Username</label>
        <input type="text" id="username-id">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labels = collectAutofillContentService["queryElementLabels"](element);

      expect(labels).toEqual(document.querySelectorAll("label[for='username-id']"));
    });

    it("returns the label of an element associated with its name value", () => {
      document.body.innerHTML = `
        <label for="username">Username</label>
        <input type="text" name="username" id="username-id">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labels = collectAutofillContentService["queryElementLabels"](element);

      expect(labels).toEqual(document.querySelectorAll("label[for='username']"));
    });

    it("removes any new lines generated for the query selector", () => {
      document.body.innerHTML = `
        <label for="username-
        id">Username</label>
        <input type="text" id="username-
        id">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labels = collectAutofillContentService["queryElementLabels"](element);

      expect(labels).toEqual(document.querySelectorAll("label[for='username-id']"));
    });
  });

  describe("createLabelElementsTag", () => {
    it("returns a string containing all the labels associated with a given input element", () => {
      const firstLabelText = "Username by name";
      const secondLabelText = "Username by ID";
      document.body.innerHTML = `
        <label for="username">${firstLabelText}</label>
        <label for="username-id">${secondLabelText}</label>
        <input type="text" name="username" id="username-id">
      `;
      const labels = document.querySelectorAll("label");
      jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText");

      const labelTag = collectAutofillContentService["createLabelElementsTag"](new Set(labels));

      expect(
        collectAutofillContentService["trimAndRemoveNonPrintableText"],
      ).toHaveBeenNthCalledWith(1, firstLabelText);
      expect(
        collectAutofillContentService["trimAndRemoveNonPrintableText"],
      ).toHaveBeenNthCalledWith(2, secondLabelText);
      expect(labelTag).toEqual(`${firstLabelText}${secondLabelText}`);
    });
  });

  describe("getAutofillFieldMaxLength", () => {
    it("returns null if the passed FormFieldElement is not an element type that has a max length property", () => {
      document.body.innerHTML = `
        <select name="country">
          <option value="US">United States</option>
          <option value="CA">Canada</option>
        </select>
      `;
      const element = document.querySelector("select") as FillableFormFieldElement;

      const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element);

      expect(maxLength).toBeNull();
    });

    it("returns a value of 999 if the passed FormFieldElement has no set maxLength value", () => {
      document.body.innerHTML = `
        <input type="text" name="username">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element);

      expect(maxLength).toEqual(999);
    });

    it("returns a value of 999 if the passed FormFieldElement has a maxLength value higher than 999", () => {
      document.body.innerHTML = `
        <input type="text" name="username" maxlength="1000">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element);

      expect(maxLength).toEqual(999);
    });

    it("returns the maxLength property of a passed FormFieldElement", () => {
      document.body.innerHTML = `
        <input type="text" name="username" maxlength="10">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element);

      expect(maxLength).toEqual(10);
    });
  });

  describe("createAutofillFieldRightLabel", () => {
    it("returns an empty string if no siblings are found", () => {
      document.body.innerHTML = `
        <input type="text" name="username">
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element);

      expect(labelTag).toEqual("");
    });

    it("returns the text content of the element's next sibling element", () => {
      document.body.innerHTML = `
        <input type="text" name="username" id="username-id">
        <label for="username-id">Username</label>
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element);

      expect(labelTag).toEqual("Username");
    });

    it("returns the text content of the element's next sibling textNode", () => {
      document.body.innerHTML = `
        <input type="text" name="username" id="username-id">
        Username
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element);

      expect(labelTag).toEqual("Username");
    });

    it("does not collect text from a sibling that contains a form field", () => {
      document.body.innerHTML = `
        <div>
          <input type="text" name="username" id="username-id">
          <div>Enter Country Code <select><option>US</option></select></div>
        </div>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element);

      expect(labelTag).toEqual("");
    });

    it("does not stop traversal at a sibling div that has no form field descendant", () => {
      document.body.innerHTML = `
        <div>
          <input type="text" name="username" id="username-id">
          <div>Helper text</div>
        </div>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element);

      expect(labelTag).toEqual("Helper text");
    });
  });

  describe("createAutofillFieldLeftLabel", () => {
    it("returns a string value of the text content associated with the previous siblings of the passed element", () => {
      document.body.innerHTML = `
        <div>
          <span>Text Content</span>
          <label for="username">Username</label>
          <input type="text" name="username" id="username-id">
        </div>
      `;
      const element = document.querySelector("input") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element);

      expect(labelTag).toEqual("Text ContentUsername");
    });

    it("does not collect text from a direct sibling that contains a form field", () => {
      document.body.innerHTML = `
        <div>
          <div>Enter Country Code <select><option>US</option></select></div>
          <input type="text" name="username" id="username-id">
        </div>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element);

      expect(labelTag).toEqual("");
    });

    it("does not collect text from a parent sibling that contains a form field", () => {
      // Exercises the parent-walk code path: the input has no direct previous
      // siblings, so the traversal walks up to the parent and checks its previous
      // sibling — which should be blocked because it contains a form field.
      document.body.innerHTML = `
        <div>
          <div>Enter Country Code <select><option>US</option></select></div>
          <div>
            <input type="text" name="username" id="username-id">
          </div>
        </div>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element);

      expect(labelTag).toEqual("");
    });

    it("does not stop traversal at a sibling div that has no form field descendant", () => {
      document.body.innerHTML = `
        <div>
          <div>Helpful label</div>
          <input type="text" name="username" id="username-id">
        </div>
      `;
      const element = document.querySelector("#username-id") as FillableFormFieldElement;

      const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element);

      expect(labelTag).toEqual("Helpful label");
    });
  });

  describe("createAutofillFieldTopLabel", () => {
    it("returns the table column header value for the passed table element", () => {
      document.body.innerHTML = `
        <table>
          <tbody>
            <tr>
              <th>Username</th>
              <th>Password</th>
              <th>Login code</th>
            </tr>
            <tr>
              <td><input type="text" name="username" /></td>
              <td><input type="password" name="password" /></td>
              <td><input type="text" name="auth-code" /></td>
            </tr>
          </tbody>
        </table>
      `;
      const targetTableCellInput = document.querySelector(
        'input[name="password"]',
      ) as HTMLInputElement;

      const targetTableCellLabel =
        collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput);

      expect(targetTableCellLabel).toEqual("Password");
    });

    it("will attempt to return the value for the previous sibling row as the label if a `th` cell is not found", () => {
      document.body.innerHTML = `
        <table>
          <tbody>
            <tr>
              <td>Username</td>
              <td>Password</td>
              <td>Login code</td>
            </tr>
            <tr>
              <td><input type="text" name="username" /></td>
              <td><input type="password" name="password" /></td>
              <td><input type="text" name="auth-code" /></td>
            </tr>
          </tbody>
        </table>
      `;
      const targetTableCellInput = document.querySelector(
        'input[name="auth-code"]',
      ) as HTMLInputElement;

      const targetTableCellLabel =
        collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput);

      expect(targetTableCellLabel).toEqual("Login code");
    });

    it("returns null for the passed table element it's parent row has no previous sibling row", () => {
      document.body.innerHTML = `
        <table>
          <tbody>
            <tr>
              <td><input type="text" name="username" /></td>
              <td><input type="password" name="password" /></td>
              <td><input type="text" name="auth-code" /></td>
            </tr>
          </tbody>
        </table>
      `;
      const targetTableCellInput = document.querySelector(
        'input[name="password"]',
      ) as HTMLInputElement;

      const targetTableCellLabel =
        collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput);

      expect(targetTableCellLabel).toEqual(null);
    });

    it("returns null if the input element is not structured within a `td` element", () => {
      document.body.innerHTML = `
        <table>
          <tbody>
            <tr>
              <td>Username</td>
              <td>Password</td>
              <td>Login code</td>
            </tr>
            <tr>
              <td><input type="text" name="username" /></td>
              <div><input type="password" name="password" /></div>
              <td><input type="text" name="auth-code" /></td>
            </tr>
          </tbody>
        </table>
      `;
      const targetTableCellInput = document.querySelector(
        'input[name="password"]',
      ) as HTMLInputElement;

      const targetTableCellLabel =
        collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput);

      expect(targetTableCellLabel).toEqual(null);
    });

    it("returns null if the index of the `td` element is larger than the length of cells in the sibling row", () => {
      document.body.innerHTML = `
        <table>
          <tbody>
            <tr>
              <td>Username</td>
              <td>Password</td>
            </tr>
            <tr>
              <td><input type="text" name="username" /></td>
              <td><input type="password" name="password" /></td>
              <td><input type="text" name="auth-code" /></td>
            </tr>
          </tbody>
        </table>
      `;
      const targetTableCellInput = document.querySelector(
        'input[name="auth-code"]',
      ) as HTMLInputElement;

      const targetTableCellLabel =
        collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput);

      expect(targetTableCellLabel).toEqual(null);
    });
  });

  describe("containsChildField", () => {
    it("returns true when the element contains an input descendant", () => {
      const div = document.createElement("div");
      div.innerHTML = `<span>Enter Country Code</span><input type="text" />`;

      expect(collectAutofillContentService["containsChildField"](div)).toBe(true);
    });

    it("returns true when the element contains a select descendant", () => {
      const div = document.createElement("div");
      div.innerHTML = `<select><option>US</option></select>`;

      expect(collectAutofillContentService["containsChildField"](div)).toBe(true);
    });

    it("returns true when the element contains a textarea descendant", () => {
      const div = document.createElement("div");
      div.innerHTML = `<textarea></textarea>`;

      expect(collectAutofillContentService["containsChildField"](div)).toBe(true);
    });

    it("returns false when the element contains no form field descendants", () => {
      const div = document.createElement("div");
      div.innerHTML = `<span>Helper text</span>`;

      expect(collectAutofillContentService["containsChildField"](div)).toBe(false);
    });

    it("returns false when the node is a text node", () => {
      const textNode = document.createTextNode("Enter Country Code");

      expect(collectAutofillContentService["containsChildField"](textNode)).toBe(false);
    });
  });

  describe("isNewSectionElement", () => {
    const validElementTags = [
      "html",
      "body",
      "button",
      "form",
      "head",
      "iframe",
      "input",
      "option",
      "script",
      "select",
      "table",
      "textarea",
    ];
    const invalidElementTags = ["div", "span"];

    describe("given a transitional element", () => {
      validElementTags.forEach((tag) => {
        const element = document.createElement(tag);

        it(`returns true if the element tag is a ${tag}`, () => {
          expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(true);
        });
      });
    });

    describe("given an non-transitional element", () => {
      invalidElementTags.forEach((tag) => {
        const element = document.createElement(tag);

        it(`returns false if the element tag is a ${tag}`, () => {
          expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(false);
        });
      });
    });

    it(`returns true if the provided element is falsy`, () => {
      expect(collectAutofillContentService["isNewSectionElement"](undefined)).toEqual(true);
    });
  });

  describe("getTextContentFromElement", () => {
    it("returns the node value for a text node", () => {
      document.body.innerHTML = `
        <div>
          <label>
            Username Label
            <input type="text" id="username-id">
          </label>
        </div>
      `;
      const element = document.querySelector("#username-id");
      const textNode = element.previousSibling;
      const parsedTextContent = collectAutofillContentService["trimAndRemoveNonPrintableText"](
        textNode.nodeValue,
      );
      jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText");

      const textContent = collectAutofillContentService["getTextContentFromElement"](textNode);

      expect(textNode.nodeType).toEqual(Node.TEXT_NODE);
      expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith(
        textNode.nodeValue,
      );
      expect(textContent).toEqual(parsedTextContent);
    });

    it("returns the text content for an element node", () => {
      document.body.innerHTML = `
        <div>
          <label for="username-id">Username Label</label>
          <input type="text" id="username-id">
        </div>
      `;
      const element = document.querySelector('label[for="username-id"]');
      jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText");

      const textContent = collectAutofillContentService["getTextContentFromElement"](element);

      expect(element.nodeType).toEqual(Node.ELEMENT_NODE);
      expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith(
        element.textContent,
      );
      expect(textContent).toEqual(element.textContent);
    });
  });

  describe("trimAndRemoveNonPrintableText", () => {
    it("returns an empty string if no text content is passed", () => {
      const textContent = collectAutofillContentService["trimAndRemoveNonPrintableText"](undefined);

      expect(textContent).toEqual("");
    });

    it("returns a trimmed string with all non-printable text removed", () => {
      const nonParsedText = `Hello!\nThis is a \t
      test   string.\x0B\x08`;

      const parsedText =
        collectAutofillContentService["trimAndRemoveNonPrintableText"](nonParsedText);

      expect(parsedText).toEqual("Hello! This is a test string.");
    });

    it("preserves extended Latin letters like Š and ć", () => {
      const text = "Šifra   ćevapčići  korisnika";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("Šifra ćevapčići korisnika");
    });

    it("removes zero-width and control characters", () => {
      const text = "Hello\u200B\u200C\u200D\u2060World\x00\x1F!";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("Hello World !");
    });

    it("removes leading and trailing whitespace", () => {
      const text = "   padded text with spaces   ";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("padded text with spaces");
    });

    it("replaces multiple whitespaces (tabs, newlines, spaces) with one space", () => {
      const text = "one\t\ntwo  \n  three\t\tfour";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("one two three four");
    });

    it("preserves emoji and symbols", () => {
      const text = "Text with emoji 🐍🚀 and ©®✓ symbols";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("Text with emoji 🐍🚀 and ©®✓ symbols");
    });

    it("handles RTL and LTR marks", () => {
      const text = "abc\u200F\u202Edеf";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("abc dеf");
    });

    it("handles mathematical unicode letters", () => {
      const text = "Unicode math: 𝒜𝒷𝒸𝒹";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("Unicode math: 𝒜𝒷𝒸𝒹");
    });

    it("removes only invisible non-printables, keeps Japanese", () => {
      const text = "これは\u200Bテストです";
      const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
      expect(result).toEqual("これは テストです");
    });
  });

  describe("recursivelyGetTextFromPreviousSiblings", () => {
    it("should find text adjacent to the target element likely to be a label", () => {
      document.body.innerHTML = `
        <div>
          Text about things
          <div>some things</div>
          <div>
            <h3>Stuff Section Header</h3>
            Other things which are also stuff
            <div style="display:none;"> Not visible text </div>
            <label for="input-tag">something else</label>
            <input id="input-tag" type="text" value="something" />
          </div>
        </div>
      `;
      const textInput = document.querySelector("#input-tag") as FormElementWithAttribute;

      const elementList: string[] =
        collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput);

      expect(elementList).toEqual([
        "something else",
        "Not visible text",
        "Other things which are also stuff",
        "Stuff Section Header",
      ]);
    });

    it("should stop looking at siblings for label values when a 'new section' element is seen", () => {
      document.body.innerHTML = `
        <div>
          Text about things
          <div>some things</div>
          <div>
            <h3>Stuff Section Header</h3>
            Other things which are also stuff
            <div style="display:none;">Not a label</div>
            <input type=text />
            <label for="input-tag">something else</label>
            <input id="input-tag" type="text" value="something" />
          </div>
        </div>
      `;

      const textInput = document.querySelector("#input-tag") as FormElementWithAttribute;
      const elementList: string[] =
        collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput);

      expect(elementList).toEqual(["something else"]);
    });

    it("should keep looking for labels in parents when there are no siblings of the target element", () => {
      document.body.innerHTML = `
        <div>
          Text about things
          <input type="text" />
          <div>some things</div>
          <div>
            <input id="input-tag" type="text" value="something" />
          </div>
        </div>
      `;

      const textInput = document.querySelector("#input-tag") as FormElementWithAttribute;
      const elementList: string[] =
        collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput);

      expect(elementList).toEqual(["some things"]);
    });

    it("should find label in parent sibling last child if no other label candidates have been encountered and there are no text nodes along the way", () => {
      document.body.innerHTML = `
        <div>
          <div>
            <div>not the most relevant things</div>
            <div>some nested things</div>
            <div>
              <input id="input-tag" type="text" value="something" />
            </div>
          </div>
        </div>
      `;

      const textInput = document.querySelector("#input-tag") as FormElementWithAttribute;
      const elementList: string[] =
        collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput);

      expect(elementList).toEqual(["some nested things"]);
    });

    it("should exit early if the target element has no parent element/node", () => {
      const textInput = document.querySelector("html") as HTMLHtmlElement;

      const elementList: string[] =
        collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput);

      expect(elementList).toEqual([]);
    });
  });

  describe("getPropertyOrAttribute", () => {
    it("returns the value of the named property of the target element if the property exists within the element", () => {
      document.body.innerHTML += '<input type="checkbox" value="userWouldLikeToCheck" checked />';
      const textInput = document.querySelector("#username") as HTMLInputElement;
      textInput.setAttribute("value", "jsmith");
      const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
      jest.spyOn(textInput, "getAttribute");
      jest.spyOn(checkboxInput, "getAttribute");

      const textInputValue = collectAutofillContentService["getPropertyOrAttribute"](
        textInput,
        "value",
      );
      const textInputId = collectAutofillContentService["getPropertyOrAttribute"](textInput, "id");
      const textInputBaseURI = collectAutofillContentService["getPropertyOrAttribute"](
        textInput,
        "baseURI",
      );
      const textInputAutofocus = collectAutofillContentService["getPropertyOrAttribute"](
        textInput,
        "autofocus",
      );
      const checkboxInputChecked = collectAutofillContentService["getPropertyOrAttribute"](
        checkboxInput,
        "checked",
      );

      expect(textInput.getAttribute).not.toHaveBeenCalled();
      expect(checkboxInput.getAttribute).not.toHaveBeenCalled();
      expect(textInputValue).toEqual("jsmith");
      expect(textInputId).toEqual("username");
      expect(textInputBaseURI).toEqual("http://localhost/");
      expect(textInputAutofocus).toEqual(false);
      expect(checkboxInputChecked).toEqual(true);
    });

    it("returns the value of the named attribute of the element if it does not exist as a property within the element", () => {
      const textInput = document.querySelector("#username") as HTMLInputElement;
      textInput.setAttribute("data-unique-attribute", "unique-value");
      jest.spyOn(textInput, "getAttribute");

      const textInputUniqueAttribute = collectAutofillContentService["getPropertyOrAttribute"](
        textInput,
        "data-unique-attribute",
      );

      expect(textInputUniqueAttribute).toEqual("unique-value");
      expect(textInput.getAttribute).toHaveBeenCalledWith("data-unique-attribute");
    });

    it("returns a null value if the element does not contain the passed attribute name as either a property or attribute value", () => {
      const textInput = document.querySelector("#username") as HTMLInputElement;
      jest.spyOn(textInput, "getAttribute");

      const textInputNonExistentAttribute = collectAutofillContentService["getPropertyOrAttribute"](
        textInput,
        "non-existent-attribute",
      );

      expect(textInputNonExistentAttribute).toEqual(null);
      expect(textInput.getAttribute).toHaveBeenCalledWith("non-existent-attribute");
    });
  });

  describe("getElementValue", () => {
    it("returns an empty string of passed input elements whose value is not set", () => {
      document.body.innerHTML += `
        <input type="checkbox" value="aTestValue" />
        <input id="hidden-input" type="hidden" />
        <span id="span-input"></span>
      `;
      const textInput = document.querySelector("#username") as HTMLInputElement;
      const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
      const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement;
      const spanInput = document.querySelector("#span-input") as HTMLInputElement;

      const textInputValue = collectAutofillContentService["getElementValue"](textInput);
      const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput);
      const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput);
      const spanInputValue = collectAutofillContentService["getElementValue"](spanInput);

      expect(textInputValue).toEqual("");
      expect(checkboxInputValue).toEqual("");
      expect(hiddenInputValue).toEqual("");
      expect(spanInputValue).toEqual("");
    });

    it("returns the value of the passed input element", () => {
      document.body.innerHTML += `
        <input type="checkbox" value="aTestValue" />
        <input id="hidden-input" type="hidden" />
        <span id="span-input">A span input value</span>
      `;
      const textInput = document.querySelector("#username") as HTMLInputElement;
      textInput.value = "jsmith";
      const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
      checkboxInput.checked = true;
      const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement;
      hiddenInput.value = "aHiddenInputValue";
      const spanInput = document.querySelector("#span-input") as HTMLInputElement;

      const textInputValue = collectAutofillContentService["getElementValue"](textInput);
      const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput);
      const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput);
      const spanInputValue = collectAutofillContentService["getElementValue"](spanInput);

      expect(textInputValue).toEqual("jsmith");
      expect(checkboxInputValue).toEqual("✓");
      expect(hiddenInputValue).toEqual("aHiddenInputValue");
      expect(spanInputValue).toEqual("A span input value");
    });

    it("return the truncated value of the passed hidden input type if the value length exceeds 256 characters", () => {
      document.body.innerHTML += `
        <input id="long-value-hidden-input" type="hidden" value="’Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe. | “Beware the Jabberwock, my son! | The jaws that bite, the claws that catch! | Beware the Jubjub bird, and shun | The frumious Bandersnatch!” | He took his vorpal sword in hand; | Long time the manxome foe he sought— | So rested he by the Tumtum tree | And stood awhile in thought. | And, as in uffish thought he stood, | The Jabberwock, with eyes of flame, | Came whiffling through the tulgey wood, | And burbled as it came! | One, two! One, two! And through and through | The vorpal blade went snicker-snack! | He left it dead, and with its head | He went galumphing back. | “And hast thou slain the Jabberwock? | Come to my arms, my beamish boy! | O frabjous day! Callooh! Callay!” | He chortled in his joy. | ’Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe." />
      `;
      const longValueHiddenInput = document.querySelector(
        "#long-value-hidden-input",
      ) as HTMLInputElement;

      const longHiddenValue =
        collectAutofillContentService["getElementValue"](longValueHiddenInput);

      expect(longHiddenValue).toEqual(
        "’Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe. | “Beware the Jabberwock, my son! | The jaws that bite, the claws that catch! | Beware the Jubjub bird, and shun | The f...SNIPPED",
      );
    });
  });

  describe("getSelectElementOptions", () => {
    it("returns the inner text and values of each `option` within the passed `select`", () => {
      document.body.innerHTML = `
        <select id="select-without-options"></select>
        <select id="select-with-options">
          <option value="1">Option: 1</option>
          <option value="b">Option - B</option>
          <option value="iii">Option III.</option>
          <option value="four"></option>
        </select>
      `;
      const selectWithOptions = document.querySelector("#select-with-options") as HTMLSelectElement;
      const selectWithoutOptions = document.querySelector(
        "#select-without-options",
      ) as HTMLSelectElement;

      const selectWithOptionsOptions =
        collectAutofillContentService["getSelectElementOptions"](selectWithOptions);
      const selectWithoutOptionsOptions =
        collectAutofillContentService["getSelectElementOptions"](selectWithoutOptions);

      expect(selectWithOptionsOptions).toEqual({
        options: [
          ["option1", "1"],
          ["optionb", "b"],
          ["optioniii", "iii"],
          [null, "four"],
        ],
      });
      expect(selectWithoutOptionsOptions).toEqual({ options: [] });
    });
  });

  describe("startMonitoring / stopMonitoring", () => {
    it("observes the document element on start", () => {
      const observeSpy = jest.spyOn(collectAutofillContentService["mutationObserver"], "observe");

      collectAutofillContentService.startMonitoring();

      expect(observeSpy).toHaveBeenCalledWith(document.documentElement, expect.any(Object));
    });

    it("is idempotent on repeated start calls", () => {
      const observeSpy = jest.spyOn(collectAutofillContentService["mutationObserver"], "observe");

      collectAutofillContentService.startMonitoring();
      collectAutofillContentService.startMonitoring();

      expect(observeSpy).toHaveBeenCalledTimes(1);
    });

    it("disconnects observers and clears caches on stop", () => {
      const mutationDisconnect = jest.spyOn(
        collectAutofillContentService["mutationObserver"],
        "disconnect",
      );
      const intersectionDisconnect = jest.spyOn(
        collectAutofillContentService["intersectionObserver"],
        "disconnect",
      );

      collectAutofillContentService.startMonitoring();
      collectAutofillContentService["_autofillFormElements"].set(
        document.createElement("form") as any,
        {} as any,
      );
      collectAutofillContentService.stopMonitoring();

      expect(mutationDisconnect).toHaveBeenCalled();
      expect(intersectionDisconnect).toHaveBeenCalled();
      expect(collectAutofillContentService["_autofillFormElements"].size).toBe(0);
    });

    it("is idempotent across repeated stop calls", () => {
      const mutationObserve = jest.spyOn(
        collectAutofillContentService["mutationObserver"],
        "observe",
      );

      collectAutofillContentService.startMonitoring();
      collectAutofillContentService.stopMonitoring();
      collectAutofillContentService.stopMonitoring();
      // A successful restart after repeated stops proves the start
      // guard cleared; behavior check rather than touching the private
      // `isMonitoring` flag directly.
      collectAutofillContentService.startMonitoring();

      expect(mutationObserve).toHaveBeenCalledTimes(2);
    });
  });

  describe("handleMutationObserverMutation", () => {
    const waitForAllMutationsToComplete = async () => {
      await waitForIdleCallback();
      await waitForIdleCallback();
      await waitForIdleCallback();
    };

    it("will set the domRecentlyMutated value to true and the noFieldsFound value to false if a form or field node has been added ", async () => {
      const form = document.createElement("form");
      document.body.appendChild(form);
      const addedNodes = document.querySelectorAll("form");
      const removedNodes = document.querySelectorAll("li");

      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: addedNodes,
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: removedNodes,
        target: document.body,
      };
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      jest.spyOn(collectAutofillContentService as any, "requirePageDetailsUpdate");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
      await waitForAllMutationsToComplete();

      expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
      expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
      expect(collectAutofillContentService["requirePageDetailsUpdate"]).toHaveBeenCalled();
    });

    it("removes cached autofill elements that are nested within a removed node", async () => {
      const form = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
      const usernameInput = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      usernameInput.setAttribute("type", "text");
      usernameInput.setAttribute("name", "username");
      form.appendChild(usernameInput);
      document.body.appendChild(form);
      const removedNodes = document.querySelectorAll("form");
      const autofillForm: AutofillForm = createAutofillFormMock({});
      const autofillField: AutofillField = createAutofillFieldMock({ opid: "field-opid" });
      collectAutofillContentService["_autofillFormElements"] = new Map([[form, autofillForm]]);
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [usernameInput, autofillField],
      ]);
      collectAutofillContentService["autofillFieldsByOpid"] = new Map<string, FormFieldElement>([
        ["field-opid", usernameInput],
      ]);
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      // The purge sweeps via !isConnected, so the form actually has to leave the document.
      document.body.removeChild(form);

      collectAutofillContentService["handleMutationObserverMutation"]([
        {
          type: "childList",
          addedNodes: null,
          attributeName: null,
          attributeNamespace: null,
          nextSibling: null,
          oldValue: null,
          previousSibling: null,
          removedNodes: removedNodes,
          target: document.body,
        },
      ]);
      await waitForAllMutationsToComplete();

      expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0);
      expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
      expect(collectAutofillContentService["autofillFieldsByOpid"].size).toEqual(0);
    });

    it("will handle updating the autofill element if any attribute mutations are encountered", async () => {
      const mutationRecord: MutationRecord = {
        type: "attributes",
        addedNodes: null,
        attributeName: "value",
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target: document.body,
      };
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      jest.spyOn(collectAutofillContentService as any, "requirePageDetailsUpdate");
      jest.spyOn(collectAutofillContentService as any, "applyAttributeMutation");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
      await waitForAllMutationsToComplete();

      expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(false);
      expect(collectAutofillContentService["noFieldsFound"]).toEqual(true);
      expect(collectAutofillContentService["requirePageDetailsUpdate"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["applyAttributeMutation"]).toHaveBeenCalledWith(
        document.body,
        "value",
      );
    });

    it("will handle window location mutations", () => {
      const mutationRecord: MutationRecord = {
        type: "attributes",
        addedNodes: null,
        attributeName: "value",
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target: document.body,
      };
      collectAutofillContentService["currentLocationHref"] = "https://someotherurl.com";
      jest.spyOn(collectAutofillContentService as any, "handleWindowLocationMutation");
      jest.spyOn(collectAutofillContentService as any, "applyAttributeMutation");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);

      expect(collectAutofillContentService["handleWindowLocationMutation"]).toHaveBeenCalled();
      expect(collectAutofillContentService["applyAttributeMutation"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["pendingAttributeMutations"].size).toBe(0);
    });

    it("schedules a full page-details rebuild for childList mutations", async () => {
      const form = document.createElement("form");
      document.body.appendChild(form);
      const addedNodes = document.querySelectorAll("form");
      const removedNodes = document.querySelectorAll("li");
      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: addedNodes,
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: removedNodes,
        target: document.body,
      };
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      jest.spyOn(collectAutofillContentService as any, "requirePageDetailsUpdate");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
      await waitForAllMutationsToComplete();

      expect(collectAutofillContentService["requirePageDetailsUpdate"]).toHaveBeenCalled();
    });

    it("skips new-shadow-root detection on attribute-only batches", () => {
      jest.useFakeTimers();
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(false);
      jest.spyOn(collectAutofillContentService as any, "collectAddedShadowRootCandidates");
      const attributeMutation: MutationRecord = {
        type: "attributes",
        addedNodes: document.querySelectorAll("nothing"),
        attributeName: "value",
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: document.querySelectorAll("nothing"),
        target: document.body,
      };

      collectAutofillContentService["handleMutationObserverMutation"]([attributeMutation]);

      expect(
        collectAutofillContentService["collectAddedShadowRootCandidates"],
      ).not.toHaveBeenCalled();
      expect(collectAutofillContentService["pendingShadowDomCheck"]).toBe(false);
      jest.useRealTimers();
    });

    it("triggers debounced page details update when mutations occur in shadow roots", () => {
      jest.useFakeTimers();
      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: document.querySelectorAll("div"),
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target: document.body,
      };
      collectAutofillContentService["currentLocationHref"] = window.location.href;

      jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(true);
      jest.spyOn(collectAutofillContentService as any, "debouncedRequirePageDetailsUpdate");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);

      expect(domQueryService.checkMutationsInShadowRoots).toHaveBeenCalledWith([mutationRecord]);
      expect(collectAutofillContentService["debouncedRequirePageDetailsUpdate"]).toHaveBeenCalled();

      jest.useRealTimers();
    });

    it("does not trigger debounced update when mutations are not in shadow roots", () => {
      jest.useFakeTimers();
      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: document.querySelectorAll("div"),
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target: document.body,
      };
      collectAutofillContentService["currentLocationHref"] = window.location.href;

      jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(false);
      jest.spyOn(collectAutofillContentService as any, "debouncedRequirePageDetailsUpdate");

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);

      expect(domQueryService.checkMutationsInShadowRoots).toHaveBeenCalledWith([mutationRecord]);
      expect(
        collectAutofillContentService["debouncedRequirePageDetailsUpdate"],
      ).not.toHaveBeenCalled();

      jest.useRealTimers();
    });

    it("schedules a debounced check for new shadow roots", () => {
      jest.useFakeTimers();
      const div = document.createElement("div");
      document.body.appendChild(div);

      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: document.querySelectorAll("div"),
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: document.querySelectorAll("nonexistent"),
        target: document.body,
      };
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      collectAutofillContentService["pendingShadowDomCheck"] = false;

      jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(false);
      jest.spyOn(domQueryService, "checkForNewShadowRoots").mockReturnValue(false);

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);

      expect(collectAutofillContentService["pendingShadowDomCheck"]).toBe(true);
      expect(collectAutofillContentService["shadowDomCheckTimeout"]).not.toBeNull();

      // Fast-forward time to trigger the debounced check
      jest.advanceTimersByTime(500);

      expect(domQueryService.checkForNewShadowRoots).toHaveBeenCalled();
      expect(collectAutofillContentService["pendingShadowDomCheck"]).toBe(false);

      jest.useRealTimers();
    });

    it("does not schedule duplicate shadow root checks when already pending", () => {
      jest.useFakeTimers();
      const div = document.createElement("div");
      document.body.appendChild(div);

      const mutationRecord: MutationRecord = {
        type: "childList",
        addedNodes: document.querySelectorAll("div"),
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: document.querySelectorAll("nonexistent"),
        target: document.body,
      };
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      collectAutofillContentService["pendingShadowDomCheck"] = true;

      const initialTimeout = setTimeout(() => {}, 500);
      collectAutofillContentService["shadowDomCheckTimeout"] = initialTimeout;

      collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);

      // Should not change the timeout since check is already pending
      expect(collectAutofillContentService["shadowDomCheckTimeout"]).toBe(initialTimeout);

      clearTimeout(initialTimeout);
      jest.useRealTimers();
    });

    it("debounces multiple rapid shadow root mutations with real timers", (done) => {
      jest.useRealTimers();

      // Use real debounce for this test
      const actualUtils = jest.requireActual("../utils");
      const realDebounce = actualUtils.debounce;

      const shadowHost = document.createElement("div");
      const shadowRoot = shadowHost.attachShadow({ mode: "open" });
      document.body.appendChild(shadowHost);

      const mutationRecord: MutationRecord = {
        type: "attributes",
        addedNodes: document.querySelectorAll("nonexistent"),
        attributeName: "value",
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: document.querySelectorAll("nonexistent"),
        target: shadowRoot,
      };

      collectAutofillContentService["currentLocationHref"] = window.location.href;

      jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(true);

      // Track actual calls to requirePageDetailsUpdate
      let callCount = 0;
      const originalRequirePageDetailsUpdate =
        collectAutofillContentService["requirePageDetailsUpdate"];
      collectAutofillContentService["requirePageDetailsUpdate"] = () => {
        callCount++;
        originalRequirePageDetailsUpdate.call(collectAutofillContentService);
      };

      // Temporarily override with real debounce
      const originalDebounced = collectAutofillContentService["debouncedRequirePageDetailsUpdate"];
      collectAutofillContentService["debouncedRequirePageDetailsUpdate"] = realDebounce(() => {
        collectAutofillContentService["requirePageDetailsUpdate"]();
      }, 300);

      // Trigger 5 rapid mutations
      for (let i = 0; i < 5; i++) {
        collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
      }

      // Should only call requirePageDetailsUpdate once after debounce
      setTimeout(() => {
        expect(callCount).toBe(1);

        // Restore original
        collectAutofillContentService["debouncedRequirePageDetailsUpdate"] = originalDebounced;
        collectAutofillContentService["requirePageDetailsUpdate"] =
          originalRequirePageDetailsUpdate;
        document.body.removeChild(shadowHost);
        done();
      }, 350);
    });

    describe("collectAddedShadowRootCandidates (filter at observation)", () => {
      const buildMutation = (added: Node[]): MutationRecord =>
        ({
          type: "childList",
          addedNodes: added as unknown as NodeList,
          attributeName: null,
          attributeNamespace: null,
          nextSibling: null,
          oldValue: null,
          previousSibling: null,
          removedNodes: document.querySelectorAll("nonexistent"),
          target: document.body,
        }) as MutationRecord;

      beforeEach(() => {
        collectAutofillContentService["pendingMutationAddedElements"].clear();
        collectAutofillContentService["pendingMutationAddedElementsOverflowed"] = false;
      });

      it("retains elements that already have a shadowRoot", () => {
        const host = document.createElement("div");
        host.attachShadow({ mode: "open" });

        collectAutofillContentService["collectAddedShadowRootCandidates"]([buildMutation([host])]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].has(host)).toBe(true);
      });

      it("retains custom-element hosts by hyphenated tag name", () => {
        const widget = document.createElement("my-widget");

        collectAutofillContentService["collectAddedShadowRootCandidates"]([
          buildMutation([widget]),
        ]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].has(widget)).toBe(
          true,
        );
      });

      it("retains a shadow host adopted from an iframe realm (PM-39772)", () => {
        const iframe = document.createElement("iframe");
        document.body.appendChild(iframe);
        // Cross-realm host: constructor comes from the iframe's realm, so
        // top-frame `host instanceof Element` returns false.
        const host = iframe.contentDocument!.createElement("foreign-host");
        host.attachShadow({ mode: "open" });

        // Precondition: confirm the cross-realm condition — if this fails,
        // jsdom didn't give us a foreign realm and the regression can't fire.
        expect(host instanceof Element).toBe(false);

        collectAutofillContentService["collectAddedShadowRootCandidates"]([buildMutation([host])]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].has(host)).toBe(true);
      });

      it("retains plain elements that have descendants", () => {
        const parent = document.createElement("section");
        parent.appendChild(document.createElement("span"));

        collectAutofillContentService["collectAddedShadowRootCandidates"]([
          buildMutation([parent]),
        ]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].has(parent)).toBe(
          true,
        );
      });

      it("skips pure-leaf, non-custom elements with no children", () => {
        const span = document.createElement("span");
        const input = document.createElement("input");

        collectAutofillContentService["collectAddedShadowRootCandidates"]([
          buildMutation([span, input]),
        ]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].size).toBe(0);
      });

      it("skips non-Element nodes (text)", () => {
        const text = document.createTextNode("hello");

        collectAutofillContentService["collectAddedShadowRootCandidates"]([buildMutation([text])]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].size).toBe(0);
      });

      it("trips the overflow flag at the cap and releases element refs", () => {
        const cap = collectAutofillContentService["pendingMutationAddedElementsCap"];
        const widgets = Array.from({ length: cap + 50 }, () => document.createElement("my-widget"));

        collectAutofillContentService["collectAddedShadowRootCandidates"]([buildMutation(widgets)]);

        expect(collectAutofillContentService["pendingMutationAddedElementsOverflowed"]).toBe(true);
        // Overflow path clears the Set immediately so refs don't linger until the debounce fires.
        expect(collectAutofillContentService["pendingMutationAddedElements"].size).toBe(0);
      });

      it("is a no-op once overflow has been tripped (later batches are ignored)", () => {
        collectAutofillContentService["pendingMutationAddedElementsOverflowed"] = true;
        const widget = document.createElement("my-widget");

        collectAutofillContentService["collectAddedShadowRootCandidates"]([
          buildMutation([widget]),
        ]);

        expect(collectAutofillContentService["pendingMutationAddedElements"].has(widget)).toBe(
          false,
        );
      });

      it("resets pending state and overflow flag after the debounced check fires", () => {
        jest.useFakeTimers();
        collectAutofillContentService["currentLocationHref"] = window.location.href;
        collectAutofillContentService["pendingShadowDomCheck"] = false;
        jest.spyOn(domQueryService, "checkMutationsInShadowRoots").mockReturnValue(false);
        jest.spyOn(domQueryService, "checkForNewShadowRoots").mockReturnValue(false);

        const widget = document.createElement("my-widget");
        document.body.appendChild(widget);
        collectAutofillContentService["pendingMutationAddedElementsOverflowed"] = true;
        collectAutofillContentService["pendingMutationAddedElements"].add(widget);

        collectAutofillContentService["handleMutationObserverMutation"]([buildMutation([widget])]);
        jest.advanceTimersByTime(500);

        expect(collectAutofillContentService["pendingMutationAddedElements"].size).toBe(0);
        expect(collectAutofillContentService["pendingMutationAddedElementsOverflowed"]).toBe(false);

        document.body.removeChild(widget);
        jest.useRealTimers();
      });
    });
  });

  describe("requirePageDetailsUpdate", () => {
    it("sets the dirty flags but does not schedule a rebuild on its own", () => {
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation");

      collectAutofillContentService["requirePageDetailsUpdate"]();

      expect(collectAutofillContentService["domRecentlyMutated"]).toBe(true);
      expect(collectAutofillContentService["noFieldsFound"]).toBe(false);
      expect(
        collectAutofillContentService["updateAutofillElementsAfterMutation"],
      ).not.toHaveBeenCalled();
    });

    it("debounced wrapper schedules a rebuild after flipping flags", () => {
      jest.useFakeTimers();
      collectAutofillContentService["domRecentlyMutated"] = false;
      jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation");

      collectAutofillContentService["debouncedRequirePageDetailsUpdate"]();
      jest.runAllTimers();

      expect(collectAutofillContentService["domRecentlyMutated"]).toBe(true);
      expect(
        collectAutofillContentService["updateAutofillElementsAfterMutation"],
      ).toHaveBeenCalled();
      jest.useRealTimers();
    });
  });

  describe("handleWindowLocationMutation", () => {
    it("will set the current location to the global location href, set the dom recently mutated flag and the no fields found flag, clear out the autofill form and field maps, and update the autofill elements after mutation", () => {
      collectAutofillContentService["currentLocationHref"] = "https://example.com/login";
      collectAutofillContentService["domRecentlyMutated"] = false;
      collectAutofillContentService["noFieldsFound"] = true;
      jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation");

      collectAutofillContentService["handleWindowLocationMutation"]();

      expect(collectAutofillContentService["currentLocationHref"]).toEqual(window.location.href);
      expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
      expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
      expect(
        collectAutofillContentService["updateAutofillElementsAfterMutation"],
      ).toHaveBeenCalled();
      expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0);
      expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
    });

    it("resets the cached targeting rules so the new URL re-fetches against the current gate state", () => {
      collectAutofillContentService["pageTargetingRules"] = [
        { category: "accountLogin", fields: { username: ["input#email"] } } as any,
      ];

      collectAutofillContentService["handleWindowLocationMutation"]();

      expect(collectAutofillContentService["pageTargetingRules"]).toBeUndefined();
    });
  });

  describe("clearCachedTargetingRules", () => {
    it("resets the cached targeting rules", () => {
      collectAutofillContentService["pageTargetingRules"] = [
        { category: "accountLogin", fields: { username: ["input#email"] } } as any,
      ];

      collectAutofillContentService.clearCachedTargetingRules();

      expect(collectAutofillContentService["pageTargetingRules"]).toBeUndefined();
    });
  });

  describe("applyAttributeMutation", () => {
    it("returns early if the target element is detached from the document", () => {
      const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
      const autofillForm = createAutofillFormMock({});
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, autofillForm],
      ]);
      jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData");

      collectAutofillContentService["applyAttributeMutation"](formElement, "id");

      expect(collectAutofillContentService["updateAutofillFormElementData"]).not.toHaveBeenCalled();
    });

    it("updates form element data when the target is in the autofillFormElements map", () => {
      const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
      document.body.appendChild(formElement);
      const autofillForm = createAutofillFormMock({});
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, autofillForm],
      ]);
      jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData");

      collectAutofillContentService["applyAttributeMutation"](formElement, "id");

      expect(collectAutofillContentService["updateAutofillFormElementData"]).toHaveBeenCalledWith(
        "id",
        formElement,
        autofillForm,
      );
    });

    it("updates field element data when the target is in the autofillFieldElements map", () => {
      const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
      document.body.appendChild(fieldElement);
      const autofillField = createAutofillFieldMock({});
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [fieldElement, autofillField],
      ]);
      jest.spyOn(collectAutofillContentService as any, "updateAutofillFieldElementData");

      collectAutofillContentService["applyAttributeMutation"](fieldElement, "id");

      expect(collectAutofillContentService["updateAutofillFieldElementData"]).toHaveBeenCalledWith(
        "id",
        fieldElement,
        autofillField,
      );
    });
  });

  describe("purgeDetachedFieldMetadata", () => {
    it("removes form/field/opid entries whose elements are no longer connected", () => {
      const attachedForm = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
      const attachedField = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(attachedForm);
      document.body.appendChild(attachedField);
      const detachedForm = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
      const detachedField = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      const attachedAutofillField = createAutofillFieldMock({ opid: "attached" });
      const detachedAutofillField = createAutofillFieldMock({ opid: "detached" });
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [attachedForm, createAutofillFormMock({})],
        [detachedForm, createAutofillFormMock({})],
      ]);
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [attachedField, attachedAutofillField],
        [detachedField, detachedAutofillField],
      ]);
      collectAutofillContentService["autofillFieldsByOpid"] = new Map<string, FormFieldElement>([
        ["attached", attachedField],
        ["detached", detachedField],
      ]);

      collectAutofillContentService["purgeDetachedFieldMetadata"]();

      expect(collectAutofillContentService["_autofillFormElements"].size).toBe(1);
      expect(collectAutofillContentService["_autofillFormElements"].has(attachedForm)).toBe(true);
      expect(collectAutofillContentService["autofillFieldElements"].size).toBe(1);
      expect(collectAutofillContentService["autofillFieldElements"].has(attachedField)).toBe(true);
      expect(collectAutofillContentService["autofillFieldsByOpid"].size).toBe(1);
      expect(collectAutofillContentService["autofillFieldsByOpid"].get("attached")).toBe(
        attachedField,
      );
    });
  });

  describe("updateAutofillFormElementData", () => {
    const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
    const autofillForm: AutofillForm = {
      opid: "1234",
      htmlName: "formEl",
      htmlID: "formEl-id",
      htmlAction: "https://example.com",
      htmlMethod: "POST",
      htmlClass: "",
      htmlAncestorHeadings: [],
    };
    const updatedAttributes = ["action", "name", "id", "method"];

    beforeEach(() => {
      collectAutofillContentService["_autofillFormElements"] = new Map([
        [formElement, autofillForm],
      ]);
    });

    updatedAttributes.forEach((attribute) => {
      it(`will update the ${attribute} value for the form element`, () => {
        jest.spyOn(collectAutofillContentService["_autofillFormElements"], "set");

        collectAutofillContentService["updateAutofillFormElementData"](
          attribute,
          formElement,
          autofillForm,
        );

        expect(collectAutofillContentService["_autofillFormElements"].set).toHaveBeenCalledWith(
          formElement,
          autofillForm,
        );
      });
    });

    it("will not update an attribute value if it is not present in the updateActions object", () => {
      jest.spyOn(collectAutofillContentService["_autofillFormElements"], "set");

      collectAutofillContentService["updateAutofillFormElementData"](
        "aria-label",
        formElement,
        autofillForm,
      );

      expect(collectAutofillContentService["_autofillFormElements"].set).not.toHaveBeenCalled();
    });
  });

  describe("updateAutofillFieldElementData", () => {
    const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
    const autofillField: AutofillField = {
      htmlClass: "value",
      htmlID: "",
      htmlName: "",
      opid: "",
      tabindex: "",
      title: "",
      viewable: false,
      elementNumber: 0,
    };
    const updatedAttributes = [
      "maxlength",
      "name",
      "id",
      "type",
      "autocomplete",
      // Note: "class" is intentionally excluded from the mutation observer attribute filter
      // to avoid callback storms on dynamic pages. htmlClass is refreshed on the next full
      // page-detail collection instead.
      "tabindex",
      "title",
      "rel",
      "checked",
      "disabled",
      "readonly",
      "data-label",
      "aria-label",
      "aria-hidden",
      "aria-disabled",
      "aria-haspopup",
      "data-stripe",
    ];

    beforeEach(() => {
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [fieldElement, autofillField],
      ]);
    });

    updatedAttributes.forEach((attribute) => {
      it(`will update the ${attribute} value for the field element`, () => {
        jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");

        collectAutofillContentService["updateAutofillFieldElementData"](
          attribute,
          fieldElement,
          autofillField,
        );

        expect(collectAutofillContentService["autofillFieldElements"].set).toHaveBeenCalledWith(
          fieldElement,
          autofillField,
        );
      });
    });

    it("will not update an attribute value if it is not present in the updateActions object", () => {
      jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");

      collectAutofillContentService["updateAutofillFieldElementData"](
        "random-attribute",
        fieldElement,
        autofillField,
      );

      expect(collectAutofillContentService["autofillFieldElements"].set).not.toHaveBeenCalled();
    });
  });

  describe("handleFormElementIntersection", () => {
    let isElementViewableSpy: jest.SpyInstance;
    let setupAutofillOverlayListenerOnFieldSpy: jest.SpyInstance;

    beforeEach(() => {
      isElementViewableSpy = jest.spyOn(
        collectAutofillContentService["domElementVisibilityService"],
        "isElementViewable",
      );
      setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
        collectAutofillContentService["autofillOverlayContentService"],
        "setupOverlayListeners",
      );
    });

    it("skips the initial intersection event for an observed element", async () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      collectAutofillContentService["elementInitializingIntersectionObserver"].add(
        formFieldElement,
      );
      const entries = [
        { target: formFieldElement, isIntersecting: true },
      ] as unknown as IntersectionObserverEntry[];

      await collectAutofillContentService["handleFormElementIntersection"](entries);

      expect(isElementViewableSpy).not.toHaveBeenCalled();
      expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
    });

    it("skips setting up the overlay listeners on a field that is not viewable", async () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      const autofillField = mock<AutofillField>();
      const entries = [
        { target: formFieldElement, isIntersecting: true },
      ] as unknown as IntersectionObserverEntry[];
      collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
      isElementViewableSpy.mockReturnValueOnce(false);

      await collectAutofillContentService["handleFormElementIntersection"](entries);

      expect(isElementViewableSpy).toHaveBeenCalledWith(formFieldElement);
      expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
    });

    it("skips setting up the inline menu listeners if the observed form field is not present in the cache", async () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      const entries = [
        { target: formFieldElement, isIntersecting: true },
      ] as unknown as IntersectionObserverEntry[];
      isElementViewableSpy.mockReturnValueOnce(true);

      await collectAutofillContentService["handleFormElementIntersection"](entries);

      expect(isElementViewableSpy).not.toHaveBeenCalled();
      expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
    });

    it("sets up the inline menu listeners on a viewable field", async () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(formFieldElement);
      const autofillField = mock<AutofillField>();
      const entries = [
        { target: formFieldElement, isIntersecting: true },
      ] as unknown as IntersectionObserverEntry[];
      isElementViewableSpy.mockReturnValueOnce(true);
      collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);

      await collectAutofillContentService["handleFormElementIntersection"](entries);

      expect(isElementViewableSpy).toHaveBeenCalledWith(formFieldElement);
      expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith(
        formFieldElement,
        autofillField,
        expect.anything(),
      );
    });
  });

  describe("setupOverlayOnField", () => {
    it("executes immediately on first call then debounces subsequent rapid calls", () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(formFieldElement);
      const autofillField = mock<AutofillField>();
      collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
      const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
        collectAutofillContentService["autofillOverlayContentService"],
        "setupOverlayListeners",
      );
      jest.useFakeTimers();

      // First call executes immediately
      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);
      expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledTimes(1);

      // Subsequent rapid calls are debounced
      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);
      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);
      expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledTimes(1);

      // After debounce delay, the next call executes immediately again
      jest.advanceTimersByTime(150);
      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);
      expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledTimes(2);

      jest.useRealTimers();
    });

    it("does not call setupOverlayListeners if the element is not in DOM", () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      // Note: not appending to document.body
      const autofillField = mock<AutofillField>();
      collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
      const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
        collectAutofillContentService["autofillOverlayContentService"],
        "setupOverlayListeners",
      );

      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);

      expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
    });

    it("does not call setupOverlayListeners if the element is not in cache", () => {
      const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(formFieldElement);
      const autofillField = mock<AutofillField>();
      // Note: not adding to autofillFieldElements cache
      const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
        collectAutofillContentService["autofillOverlayContentService"],
        "setupOverlayListeners",
      );

      collectAutofillContentService["setupOverlayOnField"](formFieldElement, autofillField);

      expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
    });
  });

  describe("stopMonitoring (deferred work cleanup)", () => {
    it("clears the updateAfterMutationIdleCallback", () => {
      jest.spyOn(window, "clearTimeout");
      collectAutofillContentService.startMonitoring();
      const callbackId = setTimeout(jest.fn, 100);
      collectAutofillContentService["updateAfterMutationIdleCallback"] = callbackId;

      collectAutofillContentService.stopMonitoring();

      expect(clearTimeout).toHaveBeenCalledWith(callbackId);
    });

    it("clears all pending overlay setup timeouts", () => {
      const formFieldElement1 = document.createElement(
        "input",
      ) as ElementWithOpId<FormFieldElement>;
      const formFieldElement2 = document.createElement(
        "input",
      ) as ElementWithOpId<FormFieldElement>;
      const clearTimeoutSpy = jest.spyOn(window, "clearTimeout");
      collectAutofillContentService.startMonitoring();
      collectAutofillContentService["pendingOverlaySetup"].set(
        formFieldElement1,
        setTimeout(jest.fn, 100),
      );
      collectAutofillContentService["pendingOverlaySetup"].set(
        formFieldElement2,
        setTimeout(jest.fn, 100),
      );

      collectAutofillContentService.stopMonitoring();

      expect(clearTimeoutSpy).toHaveBeenCalledTimes(2);
      expect(collectAutofillContentService["pendingOverlaySetup"].size).toBe(0);
    });
  });

  describe("processMutations", () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.runOnlyPendingTimers();
      jest.useRealTimers();
    });

    it("swaps the pending structures so reentrant mutations land in fresh ones", () => {
      const target = document.createElement("input");
      document.body.appendChild(target);
      collectAutofillContentService["pendingAttributeMutations"] = new Map([
        [target, new Set(["value"])],
      ]);
      collectAutofillContentService["pendingChildListUpdate"] = true;
      const originalMap = collectAutofillContentService["pendingAttributeMutations"];

      collectAutofillContentService["processMutations"]();

      expect(collectAutofillContentService["pendingAttributeMutations"]).not.toBe(originalMap);
      expect(collectAutofillContentService["pendingAttributeMutations"].size).toBe(0);
      expect(collectAutofillContentService["pendingChildListUpdate"]).toBe(false);
    });

    it("invokes the field and shadow-root purges each drain", () => {
      collectAutofillContentService["pendingChildListUpdate"] = true;
      jest.spyOn(collectAutofillContentService as any, "purgeDetachedFieldMetadata");
      jest.spyOn(domQueryService, "purgeDetachedShadowRoots");

      collectAutofillContentService["processMutations"]();
      jest.runAllTimers();

      expect(collectAutofillContentService["purgeDetachedFieldMetadata"]).toHaveBeenCalled();
      expect(domQueryService.purgeDetachedShadowRoots).toHaveBeenCalled();
    });

    it("purges detached metadata but schedules no work when nothing is pending", () => {
      collectAutofillContentService["pendingAttributeMutations"] = new Map();
      collectAutofillContentService["pendingTopLayerTargets"] = new Set();
      collectAutofillContentService["pendingChildListUpdate"] = false;
      jest.spyOn(collectAutofillContentService as any, "requirePageDetailsUpdate");
      jest.spyOn(collectAutofillContentService as any, "applyAttributeMutation");
      jest.spyOn(collectAutofillContentService as any, "purgeDetachedFieldMetadata");
      jest.spyOn(domQueryService, "purgeDetachedShadowRoots");

      collectAutofillContentService["processMutations"]();
      jest.runAllTimers();

      expect(collectAutofillContentService["requirePageDetailsUpdate"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["applyAttributeMutation"]).not.toHaveBeenCalled();
      expect(collectAutofillContentService["purgeDetachedFieldMetadata"]).toHaveBeenCalled();
      expect(domQueryService.purgeDetachedShadowRoots).toHaveBeenCalled();
    });

    it("reentrant attribute mutations during drain land in the next cycle", () => {
      const target = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(target);
      const reentryTarget = document.createElement("input") as ElementWithOpId<FormFieldElement>;
      document.body.appendChild(reentryTarget);
      collectAutofillContentService["autofillFieldElements"] = new Map([
        [target, createAutofillFieldMock({})],
        [reentryTarget, createAutofillFieldMock({})],
      ]);
      collectAutofillContentService["pendingAttributeMutations"] = new Map([
        [target, new Set(["value"])],
      ]);
      jest
        .spyOn(collectAutofillContentService as any, "applyAttributeMutation")
        .mockImplementationOnce(() => {
          // Simulate reentry: a fresh enqueue arrives mid-drain.
          collectAutofillContentService["pendingAttributeMutations"].set(
            reentryTarget,
            new Set(["id"]),
          );
        });

      collectAutofillContentService["processMutations"]();
      jest.runAllTimers();

      // Reentrant entry sits in the fresh map and waits for the next drain.
      expect(collectAutofillContentService["pendingAttributeMutations"].has(reentryTarget)).toBe(
        true,
      );
      expect(collectAutofillContentService["pendingAttributeMutations"].has(target)).toBe(false);
    });
  });

  describe("attribute mutation coalescing", () => {
    it("collapses repeated (target, attr) pairs into a single Set entry", () => {
      const target = document.createElement("input");
      document.body.appendChild(target);
      const mutation = (attrName: string): MutationRecord => ({
        type: "attributes",
        addedNodes: null,
        attributeName: attrName,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target,
      });
      collectAutofillContentService["currentLocationHref"] = window.location.href;

      collectAutofillContentService["handleMutationObserverMutation"]([
        mutation("value"),
        mutation("value"),
        mutation("id"),
      ]);

      const pending = collectAutofillContentService["pendingAttributeMutations"];
      expect(pending.size).toBe(1);
      expect(Array.from(pending.get(target)!).sort()).toEqual(["id", "value"]);
    });
  });

  describe("mutation-loop branch handling", () => {
    beforeEach(() => {
      collectAutofillContentService["currentLocationHref"] = window.location.href;
    });

    it("skips an attribute mutation whose attributeName is null", () => {
      const target = document.createElement("input");
      document.body.appendChild(target);
      const mutation: MutationRecord = {
        type: "attributes",
        addedNodes: null,
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target,
      };

      collectAutofillContentService["handleMutationObserverMutation"]([mutation]);

      expect(collectAutofillContentService["pendingAttributeMutations"].has(target)).toBe(false);
    });

    it("ignores a mutation whose type is neither attributes nor childList", () => {
      const characterData: MutationRecord = {
        type: "characterData",
        addedNodes: null,
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: null,
        target: document.createTextNode("text"),
      };

      collectAutofillContentService["handleMutationObserverMutation"]([characterData]);

      expect(collectAutofillContentService["pendingAttributeMutations"].size).toBe(0);
      expect(collectAutofillContentService["pendingTopLayerTargets"].size).toBe(0);
    });

    it("tracks an added top-layer candidate (<dialog>) as a pending target", () => {
      const container = document.createElement("div");
      const dialog = document.createElement("dialog");
      container.appendChild(dialog);
      document.body.appendChild(container);
      const mutation: MutationRecord = {
        type: "childList",
        addedNodes: container.childNodes,
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        removedNodes: document.querySelectorAll("nothing-matches-this-selector"),
        target: document.body,
      };

      collectAutofillContentService["handleMutationObserverMutation"]([mutation]);

      expect(collectAutofillContentService["pendingTopLayerTargets"].has(dialog)).toBe(true);
    });
  });

  describe("childList gate is wired into mutation handling", () => {
    const childListMutationFor = (...added: Node[]): MutationRecord => {
      const container = document.createElement("div");
      document.body.appendChild(container);
      added.forEach((n) => container.appendChild(n));
      return {
        type: "childList",
        addedNodes: container.childNodes,
        removedNodes: document.querySelectorAll("nothing-matches-this-selector"),
        attributeName: null,
        attributeNamespace: null,
        nextSibling: null,
        oldValue: null,
        previousSibling: null,
        target: document.body,
      };
    };

    beforeEach(() => {
      collectAutofillContentService["currentLocationHref"] = window.location.href;
      collectAutofillContentService["pendingChildListUpdate"] = false;
    });

    it("does not flip pendingChildListUpdate for a cosmetic childList mutation", () => {
      const cosmetic = childListMutationFor(document.createElement("div"));

      collectAutofillContentService["handleMutationObserverMutation"]([cosmetic]);

      expect(collectAutofillContentService["pendingChildListUpdate"]).toBe(false);
    });

    it("flips pendingChildListUpdate when the childList mutation contains a form field", () => {
      const input = Object.assign(document.createElement("input"), { type: "text" });
      const relevant = childListMutationFor(input);

      collectAutofillContentService["handleMutationObserverMutation"]([relevant]);

      expect(collectAutofillContentService["pendingChildListUpdate"]).toBe(true);
    });

    it("does not schedule a drain for a cosmetic-only batch (no idle-callback tax)", () => {
      const idle = jest.mocked(autofillUtils.requestIdleCallbackPolyfill);
      idle.mockClear();
      const cosmetic = childListMutationFor(document.createElement("div"));

      collectAutofillContentService["handleMutationObserverMutation"]([cosmetic]);

      expect(idle).not.toHaveBeenCalled();
    });

    it("schedules a drain when the batch contributes a relevant field", () => {
      const idle = jest.mocked(autofillUtils.requestIdleCallbackPolyfill);
      idle.mockClear();
      const input = Object.assign(document.createElement("input"), { type: "text" });
      const relevant = childListMutationFor(input);

      collectAutofillContentService["handleMutationObserverMutation"]([relevant]);

      expect(idle).toHaveBeenCalledWith(
        collectAutofillContentService["processMutations"],
        expect.objectContaining({ timeout: 500 }),
      );
    });

    it("still purges detached nodes on a cosmetic wake that schedules no drain", () => {
      const idle = jest.mocked(autofillUtils.requestIdleCallbackPolyfill);
      idle.mockClear();
      const fieldPurge = jest.spyOn(
        collectAutofillContentService as any,
        "purgeDetachedFieldMetadata",
      );
      const shadowPurge = jest.spyOn(domQueryService, "purgeDetachedShadowRoots");
      const cosmetic = childListMutationFor(document.createElement("div"));

      collectAutofillContentService["handleMutationObserverMutation"]([cosmetic]);

      expect(idle).not.toHaveBeenCalled();
      expect(fieldPurge).toHaveBeenCalled();
      expect(shadowPurge).toHaveBeenCalled();
    });
  });

  describe("mutationAddsOrRemovesFormField (relevance gate)", () => {
    // Containers we append to body for real NodeList construction; torn down after each test.
    const createdContainers: HTMLElement[] = [];

    const makeContainer = (): HTMLElement => {
      const container = document.createElement("div");
      document.body.appendChild(container);
      createdContainers.push(container);
      return container;
    };

    const buildMutation = (
      addedNodes: NodeList,
      removedNodes: NodeList = document.querySelectorAll("nothing-matches-this-selector"),
    ): MutationRecord => ({
      type: "childList",
      addedNodes,
      removedNodes,
      attributeName: null,
      attributeNamespace: null,
      nextSibling: null,
      oldValue: null,
      previousSibling: null,
      target: document.body,
    });

    afterEach(() => {
      while (createdContainers.length) {
        const container = createdContainers.pop()!;
        if (container.parentNode) {
          container.parentNode.removeChild(container);
        }
      }
    });

    const expectGate = (mutation: MutationRecord, admit: boolean) =>
      expect(collectAutofillContentService["mutationAddsOrRemovesFormField"](mutation)).toBe(admit);

    const mutationForChild = (child: Node): MutationRecord => {
      const container = makeContainer();
      container.appendChild(child);
      return buildMutation(container.childNodes);
    };

    const mutationForInput = (configure: (input: HTMLInputElement) => void): MutationRecord => {
      const input = document.createElement("input");
      configure(input);
      return mutationForChild(input);
    };

    describe("admits via formFieldQueryString match on the node itself", () => {
      it("admits <select>", () =>
        expectGate(mutationForChild(document.createElement("select")), true));
      it("admits <textarea>", () =>
        expectGate(mutationForChild(document.createElement("textarea")), true));

      it("admits <span data-bwautofill>", () => {
        const span = document.createElement("span");
        span.setAttribute("data-bwautofill", "");
        expectGate(mutationForChild(span), true);
      });

      it("admits a type-less <input> (no type attribute satisfies every :not([type=X]) clause)", () => {
        const input = document.createElement("input");
        expect(input.getAttribute("type")).toBeNull();
        expectGate(mutationForChild(input), true);
      });

      it.each(["text", "email", "password", "number", "tel"])('admits <input type="%s">', (type) =>
        expectGate(
          mutationForInput((i) => (i.type = type)),
          true,
        ),
      );
    });

    describe("rejects bare elements with no autofill descendants", () => {
      it("rejects <div>", () => expectGate(mutationForChild(document.createElement("div")), false));
      it("rejects <p>", () => expectGate(mutationForChild(document.createElement("p")), false));
      it("rejects <span> without data-bwautofill", () =>
        expectGate(mutationForChild(document.createElement("span")), false));

      // Consolidation deliberately drops the FORM-tag fast-path: empty forms have no fields,
      // and any later child-append mutation trips the gate via the descendant query.
      it("rejects an empty <form>", () =>
        expectGate(mutationForChild(document.createElement("form")), false));
    });

    describe("rejects inputs whose type is in ignoredInputTypes", () => {
      // Structural — never autofill targets.
      it.each(["hidden", "submit", "reset", "button", "image", "file"])(
        'rejects <input type="%s">',
        (type) =>
          expectGate(
            mutationForInput((i) => (i.type = type)),
            false,
          ),
      );

      // Format-restricted text types. Consolidation widened these from admit to reject:
      // the gate now uses the same exclusion set as collection, so the rebuild it used to
      // schedule for these would have produced zero new fields anyway.
      it.each(["search", "url", "date", "time", "datetime-local", "week", "color", "range"])(
        'rejects <input type="%s">',
        (type) =>
          expectGate(
            mutationForInput((i) => (i.type = type)),
            false,
          ),
      );
    });

    describe("rejects nodes carrying data-bwignore", () => {
      // Consolidating onto formFieldQueryString means data-bwignore-marked elements no longer
      // trip the gate. Safe because they'd be filtered out during collection anyway.
      it('rejects <input type="text" data-bwignore>', () =>
        expectGate(
          mutationForInput((i) => {
            i.type = "text";
            i.setAttribute("data-bwignore", "");
          }),
          false,
        ));

      it("rejects <select data-bwignore>", () => {
        const select = document.createElement("select");
        select.setAttribute("data-bwignore", "");
        expectGate(mutationForChild(select), false);
      });

      it("rejects <textarea data-bwignore>", () => {
        const textarea = document.createElement("textarea");
        textarea.setAttribute("data-bwignore", "");
        expectGate(mutationForChild(textarea), false);
      });

      it("rejects a <div> wrapping only a data-bwignore field (descendant inherits exclusion)", () => {
        const wrapper = document.createElement("div");
        const input = document.createElement("input");
        input.type = "text";
        input.setAttribute("data-bwignore", "");
        wrapper.appendChild(input);
        expectGate(mutationForChild(wrapper), false);
      });
    });

    describe("admits via descendant match", () => {
      it("admits a <div> wrapping a text input", () => {
        const wrapper = document.createElement("div");
        const input = document.createElement("input");
        input.type = "text";
        wrapper.appendChild(input);
        expectGate(mutationForChild(wrapper), true);
      });

      it("admits a <div> containing both a hidden and a text input (text input matches)", () => {
        const wrapper = document.createElement("div");
        const hidden = document.createElement("input");
        hidden.type = "hidden";
        const text = document.createElement("input");
        text.type = "text";
        wrapper.append(hidden, text);
        expectGate(mutationForChild(wrapper), true);
      });

      it("admits a <div> wrapping a <span data-bwautofill>", () => {
        const wrapper = document.createElement("div");
        const span = document.createElement("span");
        span.setAttribute("data-bwautofill", "");
        wrapper.appendChild(span);
        expectGate(mutationForChild(wrapper), true);
      });

      it("admits a <form> containing a text input (via descendant, not the FORM tag itself)", () => {
        const form = document.createElement("form");
        const input = document.createElement("input");
        input.type = "text";
        form.appendChild(input);
        expectGate(mutationForChild(form), true);
      });

      it("rejects a <form> whose only descendant is an excluded input", () => {
        const form = document.createElement("form");
        const hidden = document.createElement("input");
        hidden.type = "hidden";
        form.appendChild(hidden);
        expectGate(mutationForChild(form), false);
      });
    });

    describe("processes removedNodes identically to addedNodes", () => {
      const emptyNodeList = (): NodeList =>
        document.querySelectorAll("nothing-matches-this-selector");

      it("admits when a text input appears only in removedNodes", () => {
        const container = makeContainer();
        const input = document.createElement("input");
        input.type = "text";
        container.appendChild(input);
        expectGate(buildMutation(emptyNodeList(), container.childNodes), true);
      });

      it("rejects when only an empty <form> appears in removedNodes", () => {
        const container = makeContainer();
        container.appendChild(document.createElement("form"));
        expectGate(buildMutation(emptyNodeList(), container.childNodes), false);
      });
    });
  });

  describe("nodeListContainsFormField", () => {
    it("skips non-element nodes (text, comment) and returns false when no element is relevant", () => {
      const container = document.createElement("div");
      container.append(
        document.createTextNode("hello"),
        document.createComment("a comment"),
        document.createElement("p"),
      );
      document.body.appendChild(container);

      expect(collectAutofillContentService["nodeListContainsFormField"](container.childNodes)).toBe(
        false,
      );
      document.body.removeChild(container);
    });

    it("returns false for an empty NodeList", () => {
      const empty = document.querySelectorAll("nothing-matches-this-selector");
      expect(collectAutofillContentService["nodeListContainsFormField"](empty)).toBe(false);
    });

    it("short-circuits on the first relevant node and returns true", () => {
      const container = document.createElement("div");
      const irrelevant = document.createElement("p");
      const input = document.createElement("input");
      input.type = "text";
      container.append(irrelevant, input);
      document.body.appendChild(container);

      expect(collectAutofillContentService["nodeListContainsFormField"](container.childNodes)).toBe(
        true,
      );
      document.body.removeChild(container);
    });
  });
});
