import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { FakeDocument, type FakeElement, fakeStorage } from "./fakeDom.ts";
import {
  ONBOARDING_STEPS,
  ONBOARDING_STORAGE_KEY,
  hasSeenOnboarding,
  markOnboardingSeen,
  mountOnboarding,
} from "../../ui/onboarding.ts";
import type { OnboardingStorage } from "../../ui/onboarding.ts";

function setup(storage: OnboardingStorage | null, coarsePointer = false, force = false) {
  const doc = new FakeDocument();
  const host = doc.createElement("div");
  doc.body.append(host);
  const finished: string[] = [];
  const coach = mountOnboarding(host as unknown as HTMLElement, {
    coarsePointer,
    storage,
    force,
    onFinish: (reason) => finished.push(reason),
  });
  return { doc, host, coach, finished, root: coach.root as unknown as FakeElement | null };
}

describe("the first-run coach", () => {
  it("teaches the two verbs the product is, and the control surface that reaches them", () => {
    // The product is "fly the board" and "walk into the studio", and before this
    // file neither was ever stated on screen at any tier in any layout.
    const verbs = ONBOARDING_STEPS.map((step) => step.verb.toLowerCase());
    assert.equal(ONBOARDING_STEPS.length, 3);
    assert.ok(verbs.some((verb) => verb.includes("fly")));
    assert.ok(verbs.some((verb) => verb.includes("walk into")));
    assert.ok(verbs.some((verb) => verb.includes("controls")));
  });

  it("gives every step a touch sentence that is a different act, not a translation", () => {
    // The desktop's key strip is `display: none` below 600px, so on a phone this
    // is the only place the controls are ever named.
    for (const step of ONBOARDING_STEPS) {
      assert.ok(step.touch.trim().length > 10, `${step.id} has no touch sentence`);
      assert.notEqual(step.touch, step.pointer, `${step.id} just restates its pointer sentence`);
    }
    const dock = ONBOARDING_STEPS.find((step) => step.id === "dock");
    // The joystick is the control a phone visitor otherwise had no way to find.
    assert.match(dock?.touch ?? "", /joystick/);
  });

  it("appears when storage is empty", () => {
    const storage = fakeStorage();
    const { coach, root, host } = setup(storage);
    assert.equal(coach.visible, true);
    assert.ok(root);
    assert.equal(host.children.length, 1);
    assert.match(root.text(), /Fly the board/);
  });

  it("does not appear on the second mount", () => {
    const storage = fakeStorage();
    const first = setup(storage);
    assert.equal(first.coach.visible, true);

    const second = setup(storage);
    assert.equal(second.coach.visible, false);
    assert.equal(second.root, null);
    assert.equal(second.host.children.length, 0, "a returning visitor's page carries no trace");
  });

  it("records the visit at mount, not at completion", () => {
    /*
     * Deliberate. Somebody who read the first card and went straight to dragging
     * the map has been onboarded; a coach that reappears next visit because they
     * did not press "Next" three times is arguing with its own success
     * condition.
     */
    const storage = fakeStorage();
    setup(storage);
    assert.equal(storage.map.has(ONBOARDING_STORAGE_KEY), true);
  });

  it("still renders when every localStorage accessor throws", () => {
    // Safari private mode, and any embed with third-party storage blocked. An
    // onboarding card is not worth a blank page.
    const storage = fakeStorage("throwing");
    assert.doesNotThrow(() => hasSeenOnboarding(storage));
    assert.equal(hasSeenOnboarding(storage), false);
    assert.doesNotThrow(() => markOnboardingSeen(storage));

    const { coach, root } = setup(storage);
    assert.equal(coach.visible, true);
    assert.ok(root);
    // And it keeps working: a throwing store means it will be offered again next
    // visit, which is survivable in a way that never showing it is not.
    const again = setup(storage);
    assert.equal(again.coach.visible, true);
  });

  it("treats a missing store the same way", () => {
    assert.equal(hasSeenOnboarding(null), false);
    assert.doesNotThrow(() => markOnboardingSeen(null));
    const { coach } = setup(null);
    assert.equal(coach.visible, true);
  });

  it("steps forward, marks progress, and finishes on the last card", () => {
    const { coach, root, finished } = setup(fakeStorage());
    assert.ok(root);
    assert.equal(coach.index(), 0);

    coach.next();
    assert.equal(coach.index(), 1);
    assert.match(root.text(), /Take the controls/);

    coach.next();
    assert.equal(coach.index(), 2);
    assert.match(root.text(), /Walk into the studio/);
    assert.match(root.text(), /Start exploring/);

    coach.next();
    assert.equal(coach.index(), -1);
    assert.equal(root.hidden, true);
    assert.deepEqual(finished, ["finished"]);

    // Idempotent: a double-press of the last button must not fire twice.
    coach.next();
    assert.deepEqual(finished, ["finished"]);
  });

  it("is skippable from any card, in one press", () => {
    const { coach, root, finished } = setup(fakeStorage());
    assert.ok(root);
    coach.next();
    const skip = root.querySelector(".tera-coach__skip");
    assert.ok(skip);
    skip.click();
    assert.equal(root.hidden, true);
    assert.equal(coach.index(), -1);
    assert.deepEqual(finished, ["skipped"]);
  });

  it("rings the control each step is naming, and stops when it is done", () => {
    // A coach that greys the scene to point at the scene has explained nothing.
    // This is a card in a corner plus one attribute on <body>.
    const { doc, coach } = setup(fakeStorage());
    assert.equal(doc.body.getAttribute("data-coach"), null, "step one is about the whole scene");
    coach.next();
    assert.equal(doc.body.getAttribute("data-coach"), "mode-dock");
    coach.next();
    assert.equal(doc.body.getAttribute("data-coach"), "enter");
    coach.skip();
    assert.equal(doc.body.getAttribute("data-coach"), null);
  });

  it("leads with the touch sentence on a coarse pointer", () => {
    const withMouse = setup(fakeStorage(), false);
    assert.ok(withMouse.root);
    assert.match(withMouse.root.text(), /scroll to zoom/);

    const withThumb = setup(fakeStorage(), true);
    assert.ok(withThumb.root);
    assert.match(withThumb.root.text(), /pinch to zoom/);
  });

  it("can be shown again on request, without a second copy of the stylesheet", () => {
    const storage = fakeStorage();
    const first = setup(storage);
    first.coach.dispose();
    const again = setup(storage, false, true);
    assert.equal(again.coach.visible, true);
    assert.equal(again.doc.head.querySelectorAll("style").length, 1);
  });

  it("leaves nothing behind on dispose", () => {
    const { doc, host, coach } = setup(fakeStorage());
    coach.next();
    assert.equal(doc.body.getAttribute("data-coach"), "mode-dock");
    coach.dispose();
    assert.equal(host.children.length, 0);
    assert.equal(doc.body.getAttribute("data-coach"), null);
    assert.equal(doc.head.querySelectorAll("style").length, 0);
  });
});
