/**
 * @vitest-environment jsdom
 */
import { afterEach, describe, expect, it } from "vite-plus/test";
import * as Y from "@y/y";

import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import { VersioningExtension } from "../../extensions/Versioning/index.js";
import type { VersioningEndpoints } from "../../extensions/Versioning/index.js";
import { withCollaboration } from "./index.js";
import { createYjsVersioningAdapter } from "./Versioning.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Simple in-memory Yjs versioning endpoints for tests.
 * Stores snapshots and their binary content in plain Maps.
 */
function createInMemoryYjsEndpoints(): VersioningEndpoints<Y.Type, Uint8Array> {
  const snapshots = new Map<
    string,
    {
      id: string;
      name?: string;
      createdAt: number;
      updatedAt: number;
      restoredFromSnapshotId?: string;
    }
  >();
  const contents = new Map<string, Uint8Array>();

  return {
    list: async () =>
      [...snapshots.values()].sort((a, b) => b.createdAt - a.createdAt),
    create: async (fragment, options) => {
      const snapshot = {
        id: crypto.randomUUID(),
        name: options?.name,
        createdAt: Date.now(),
        updatedAt: Date.now(),
        restoredFromSnapshotId: options?.restoredFromSnapshot?.id
          ? String(options.restoredFromSnapshot.id)
          : undefined,
      };
      contents.set(snapshot.id, Y.encodeStateAsUpdateV2(fragment.doc!));
      snapshots.set(snapshot.id, snapshot);
      return snapshot;
    },
    getContent: async (snapshot) => {
      const data = contents.get(String(snapshot.id));
      if (!data) {
        throw new Error(`Snapshot ${String(snapshot.id)} not found`);
      }
      return data;
    },
    restore: async (fragment, snapshot) => {
      // Create backup
      const backup = {
        id: crypto.randomUUID(),
        name: "Backup",
        createdAt: Date.now(),
        updatedAt: Date.now(),
      };
      contents.set(backup.id, Y.encodeStateAsUpdateV2(fragment.doc!));
      snapshots.set(backup.id, backup);

      const snapshotContent = contents.get(String(snapshot.id))!;
      const tempDoc = new Y.Doc();
      Y.applyUpdateV2(tempDoc, snapshotContent);

      const restored = {
        id: crypto.randomUUID(),
        name: "Restored Snapshot",
        createdAt: Date.now(),
        updatedAt: Date.now(),
        restoredFromSnapshotId: String(snapshot.id),
      };
      contents.set(restored.id, Y.encodeStateAsUpdateV2(tempDoc));
      snapshots.set(restored.id, restored);
      tempDoc.destroy();

      return snapshotContent;
    },
    rename: async (snapshot, name) => {
      const s = snapshots.get(String(snapshot.id));
      if (!s) {
        throw new Error(`Snapshot ${String(snapshot.id)} not found`);
      }
      s.name = name;
      s.updatedAt = Date.now();
    },
  };
}

/** Create a collaborative editor with versioning, mounted to a jsdom div. */
function createCollabEditor(opts?: { withVersioning?: boolean }) {
  const doc = new Y.Doc();
  const fragment = doc.get("doc");
  const endpoints = createInMemoryYjsEndpoints();

  const editor = BlockNoteEditor.create(
    withCollaboration({
      collaboration: {
        fragment,
        user: { name: "Test User", color: "#ff0000" },
        provider: undefined,
        versioningEndpoints:
          opts?.withVersioning !== false ? endpoints : undefined,
      },
    }),
  );

  const div = document.createElement("div");
  editor.mount(div);

  return { editor, doc, fragment, endpoints };
}

/** Clean up an editor and its Y.Doc. */
function cleanup(ctx: { editor: BlockNoteEditor<any, any, any>; doc: Y.Doc }) {
  ctx.editor.unmount();
  ctx.doc.destroy();
}

/** Get the editor's current ProseMirror doc text content. */
function getEditorText(editor: BlockNoteEditor<any, any, any>): string {
  return editor.prosemirrorState.doc.textContent;
}

// ---------------------------------------------------------------------------
// Tests: createYjsVersioningAdapter (unit-level)
// ---------------------------------------------------------------------------

describe("createYjsVersioningAdapter", () => {
  let ctx: ReturnType<typeof createCollabEditor>;

  afterEach(() => {
    if (ctx) {
      cleanup(ctx);
    }
  });

  it("getCurrentDocument returns the fragment passed to the adapter", () => {
    ctx = createCollabEditor();
    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);
    const state = adapter.getCurrentDocument();

    expect(state).toBe(ctx.fragment);
    expect(state.doc).toBe(ctx.doc);
  });

  it("enterPreview reconfigures the editor to show snapshot content", () => {
    ctx = createCollabEditor();

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Original content" },
    ]);
    const snapshotData = Y.encodeStateAsUpdateV2(ctx.doc);

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Modified content" },
    ]);

    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);
    adapter.preview.enterPreview(snapshotData);

    expect(getEditorText(ctx.editor)).toContain("Original content");
    expect(getEditorText(ctx.editor)).not.toContain("Modified");
  });

  it("exitPreview resumes sync, showing the live document", () => {
    ctx = createCollabEditor();

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Snapshot state" },
    ]);
    const snapshotData = Y.encodeStateAsUpdateV2(ctx.doc);

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Current state" },
    ]);

    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);
    adapter.preview.enterPreview(snapshotData);
    expect(getEditorText(ctx.editor)).toContain("Snapshot state");

    adapter.preview.exitPreview();
    expect(getEditorText(ctx.editor)).toContain("Current state");
  });

  it("successive enterPreview calls switch between snapshots", () => {
    ctx = createCollabEditor();

    // Create snapshot A
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Snapshot A" },
    ]);
    const snapshotA = Y.encodeStateAsUpdateV2(ctx.doc);

    // Create snapshot B
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Snapshot B" },
    ]);
    const snapshotB = Y.encodeStateAsUpdateV2(ctx.doc);

    // Move to current content
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Current" },
    ]);

    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);

    // Preview A
    adapter.preview.enterPreview(snapshotA);
    expect(getEditorText(ctx.editor)).toContain("Snapshot A");

    // Switch to B without exiting first
    adapter.preview.enterPreview(snapshotB);
    expect(getEditorText(ctx.editor)).toContain("Snapshot B");

    // Exit should restore the live doc
    adapter.preview.exitPreview();
    expect(getEditorText(ctx.editor)).toContain("Current");
  });

  it("exitPreview is a no-op when not previewing", () => {
    ctx = createCollabEditor();
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Content" },
    ]);

    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);

    // Should not throw or change anything
    adapter.preview.exitPreview();
    expect(getEditorText(ctx.editor)).toContain("Content");
  });

  it("applyRestore is a no-op (server-side restore propagates via live sync)", () => {
    ctx = createCollabEditor();
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Content" },
    ]);

    const adapter = createYjsVersioningAdapter(ctx.editor, ctx.fragment);

    // Should not throw and should leave the live document untouched.
    expect(() => adapter.preview.applyRestore(new Uint8Array())).not.toThrow();
    expect(getEditorText(ctx.editor)).toContain("Content");
  });
});

// ---------------------------------------------------------------------------
// Tests: Full integration with VersioningExtension + localStorageEndpoints
// ---------------------------------------------------------------------------

describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)", () => {
  let ctx: ReturnType<typeof createCollabEditor>;

  afterEach(() => {
    if (ctx) {
      cleanup(ctx);
    }
  });

  it("previews a snapshot, showing the old content in the editor", async () => {
    ctx = createCollabEditor();
    const versioning = ctx.editor.getExtension(VersioningExtension)!;

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Snapshot content" },
    ]);
    const snapshot = await versioning.create!({ name: "v1" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Current content" },
    ]);

    await versioning.previewSnapshot(snapshot.id);

    expect(versioning.store.state.previewedSnapshotId).toBe(snapshot.id);
    expect(getEditorText(ctx.editor)).toContain("Snapshot content");
    expect(getEditorText(ctx.editor)).not.toContain("Current");
  });

  it("exits preview and returns to live document", async () => {
    ctx = createCollabEditor();
    const versioning = ctx.editor.getExtension(VersioningExtension)!;

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Saved state" },
    ]);
    const snapshot = await versioning.create!({ name: "v1" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Live state" },
    ]);

    await versioning.previewSnapshot(snapshot.id);
    versioning.exitPreview();

    expect(getEditorText(ctx.editor)).toContain("Live state");
  });

  it("full workflow: create, browse, preview, exit", async () => {
    ctx = createCollabEditor();
    const versioning = ctx.editor.getExtension(VersioningExtension)!;

    // Create two versions
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Version 1" },
    ]);
    const v1 = await versioning.create!({ name: "v1" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Version 2" },
    ]);
    const v2 = await versioning.create!({ name: "v2" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Current state" },
    ]);

    // List and verify ordering
    const list = await versioning.list();
    expect(list).toHaveLength(2);
    expect(list[0]!.id).toBe(v2.id);

    // Browse previews
    await versioning.previewSnapshot(v1.id);
    expect(getEditorText(ctx.editor)).toContain("Version 1");

    await versioning.previewSnapshot(v2.id, { compareTo: v1.id });
    expect(getEditorText(ctx.editor).length).toBeGreaterThan(0);

    // Exit back to live
    versioning.exitPreview();
    expect(getEditorText(ctx.editor)).toContain("Current state");
  });

  it("restore resolves with the restored snapshot content", async () => {
    ctx = createCollabEditor();
    const versioning = ctx.editor.getExtension(VersioningExtension)!;

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Content" },
    ]);
    const snap = await versioning.create!({ name: "v1" });

    // applyRestore is a no-op for the Yjs adapter (the backend applies the
    // restore and the change propagates over live sync), so restore
    // resolves with the snapshot content returned by the endpoint.
    const content = await versioning.restore!(snap.id);
    expect(content).toBeInstanceOf(Uint8Array);
  });

  it("previewing multiple snapshots and switching between them", async () => {
    ctx = createCollabEditor();
    const versioning = ctx.editor.getExtension(VersioningExtension)!;

    // Create three versions at different points
    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Version 1" },
    ]);
    const v1 = await versioning.create!({ name: "v1" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Version 2" },
    ]);
    const v2 = await versioning.create!({ name: "v2" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Version 3" },
    ]);
    await versioning.create!({ name: "v3" });

    ctx.editor.replaceBlocks(ctx.editor.document, [
      { type: "paragraph", content: "Current live" },
    ]);

    // Preview older, then newer
    await versioning.previewSnapshot(v1.id);
    expect(getEditorText(ctx.editor)).toContain("Version 1");

    await versioning.previewSnapshot(v2.id);
    expect(getEditorText(ctx.editor)).toContain("Version 2");
    expect(versioning.store.state.previewedSnapshotId).toBe(v2.id);

    // Exit back to live
    versioning.exitPreview();
    expect(getEditorText(ctx.editor)).toContain("Current live");
  });
});
