import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createPresenceIndicator } from "../realtime/index.ts";

type Listener = () => void;

class FakeElement {
  readonly children: FakeElement[] = [];
  readonly attributes = new Map<string, string>();
  readonly listeners = new Map<string, Listener[]>();
  parentElement: FakeElement | null = null;
  className = "";
  textContent = "";
  hidden = false;
  type = "";
  readonly ownerDocument: FakeDocument;
  constructor(ownerDocument: FakeDocument) { this.ownerDocument = ownerDocument; }
  append(...nodes: FakeElement[]): void {
    for (const node of nodes) { node.parentElement = this; this.children.push(node); }
  }
  setAttribute(name: string, value: string): void { this.attributes.set(name, value); }
  getAttribute(name: string): string | null { return this.attributes.get(name) ?? null; }
  addEventListener(type: string, listener: Listener): void {
    const listeners = this.listeners.get(type);
    if (listeners) listeners.push(listener); else this.listeners.set(type, [listener]);
  }
  click(): void { for (const listener of this.listeners.get("click") ?? []) listener(); }
  remove(): void {
    if (!this.parentElement) return;
    const index = this.parentElement.children.indexOf(this);
    if (index >= 0) this.parentElement.children.splice(index, 1);
    this.parentElement = null;
  }
  text(): string { return this.textContent + this.children.map((child) => child.text()).join(""); }
}

class FakeDocument {
  createElement(): FakeElement { return new FakeElement(this); }
}

function setup(onRetry?: () => void) {
  const document = new FakeDocument();
  const container = document.createElement();
  const indicator = createPresenceIndicator({
    container: container as unknown as HTMLElement,
    onRetry,
  });
  return { container, indicator, root: indicator.root as unknown as FakeElement };
}

describe("hosted presence indicator", () => {
  it("is private and off by default with a polite atomic status", () => {
    const { indicator, root } = setup();
    assert.deepEqual(indicator.state(), { signedIn: false, connection: "off", nearbyPeerCount: 0 });
    assert.equal(root.getAttribute("data-presence"), "off");
    const status = root.children[0]!;
    assert.equal(status.getAttribute("role"), "status");
    assert.equal(status.getAttribute("aria-live"), "polite");
    assert.equal(status.getAttribute("aria-atomic"), "true");
    assert.equal(root.text(), "Hosted presence is off.Retry presence");
    assert.equal(root.children[1]?.hidden, true);
  });

  it("renders only signed-in connection state and aggregate nearby count", () => {
    const { indicator, root } = setup();
    const source = { signedIn: true, connection: "live" as const, nearbyPeerCount: 7 };
    assert.deepEqual(indicator.update(source), source);
    source.nearbyPeerCount = 99;
    assert.deepEqual(indicator.state(), { signedIn: true, connection: "live", nearbyPeerCount: 7 });
    assert.match(root.text(), /live · 7 nearby peers/);
    assert.doesNotMatch(root.text(), /actor|token|display name/i);

    indicator.update({ signedIn: false, connection: "live", nearbyPeerCount: 400 });
    assert.deepEqual(indicator.state(), { signedIn: false, connection: "off", nearbyPeerCount: 0 });
    assert.equal(root.text(), "Hosted presence is off.Retry presence");
  });

  it("shows retry only while signed-in offline/reconnecting and emits intent only", () => {
    let retries = 0;
    const { indicator, root } = setup(() => { retries += 1; });
    const retry = root.children[1]!;
    retry.click();
    assert.equal(retries, 0);
    indicator.update({ signedIn: true, connection: "connecting", nearbyPeerCount: 0 });
    assert.equal(retry.hidden, true);
    indicator.update({ signedIn: true, connection: "reconnecting", nearbyPeerCount: 12 });
    assert.equal(retry.hidden, false);
    assert.match(root.text(), /reconnecting/);
    assert.doesNotMatch(root.text(), /12/);
    retry.click();
    indicator.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
    retry.click();
    assert.equal(retries, 2);
    assert.equal("fetch" in indicator, false);
  });

  it("rejects invalid aggregate state and disposes idempotently", () => {
    const { container, indicator, root } = setup(() => assert.fail("disposed retry fired"));
    assert.throws(() => indicator.update({ signedIn: true, connection: "live", nearbyPeerCount: -1 }), /integer/);
    assert.throws(() => indicator.update({
      signedIn: true, connection: "secret" as "live", nearbyPeerCount: 0,
    }), /connection/);
    indicator.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
    indicator.dispose();
    indicator.dispose();
    root.children[1]!.click();
    assert.equal(container.children.length, 0);
    assert.deepEqual(indicator.state(), { signedIn: false, connection: "off", nearbyPeerCount: 0 });
    assert.deepEqual(
      indicator.update({ signedIn: true, connection: "live", nearbyPeerCount: 2 }),
      { signedIn: false, connection: "off", nearbyPeerCount: 0 },
    );
  });
});
