import { resolveToolcraftPanelSurfaceContributions } from "./contributions/panel-surface-contributions";
import { snapshotToolcraftModuleData } from "./contract/module-data";
import * as React from "react";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { resolveToolcraftModuleDependencies } from "./resolution/resolve-module-dependencies";
import { resolveToolcraftArtifactSettingsContributions } from "./contributions/artifact-settings-contributions";
import { assertUniqueToolcraftCommandOwners } from "./contract/command-handlers";
import { createToolcraftPersistenceCodec } from "./contract/persistence-codec";
import { resolveToolcraftModuleBindings } from "./contract/module-bindings";
import { normalizeToolcraftSettingsContribution } from "./contract/control-section-contribution";
import { gridSettingsModule } from "./testing/extensibility/settings-module";
import { notesCommands, notesModule, notesPersistence, NotesSurface, reduceNotes } from "./testing/extensibility/notes-module";
import { imageExportModule } from "./built-ins/image-export/image-export-module";
describe("module extension mechanisms", () => {
it("resolves and materializes unrelated settings with arbitrary fields and existing control types", () => {
const definition = gridSettingsModule(24);
const resolved = resolveToolcraftModuleDependencies([definition], []);
expect(resolved.modules.map(module => module.id)).toEqual(["grid-settings"]);
const imageSettings = imageExportModule().contributions.filter(contribution => contribution.kind === "control-section");
const sections = resolveToolcraftArtifactSettingsContributions([...imageSettings, ...definition.contributions]);
expect(sections.map(section => section.id)).toEqual(["runtime.grid-settings", "runtime.image-export"]);
expect(sections[0]?.controls.spacing).toMatchObject({ type: "slider", defaultValue: 24, target: "grid.spacing" });
expect(Object.keys(sections[0]!.controls)).toEqual(["spacing", "color"]);
expect(Object.isFrozen(sections[0]!.controls.spacing)).toBe(true);
expect(() => gridSettingsModule(65)).toThrow("Grid spacing");
});
it("checks settings targets/layout and rejects executable declarations before materialization", () => {
const contribution = gridSettingsModule().contributions[0]!;
expect(() => normalizeToolcraftSettingsContribution({
...contribution, section: {
...contribution.section, controls: { ...contribution.section.controls, duplicate: contribution.section.controls.spacing! },
}
})).toThrow("Duplicate control target");
expect(() => normalizeToolcraftSettingsContribution({
...contribution, section: {
...contribution.section, layoutGroups: [{ controls: ["missing"], layout: "inline" }],
}
})).toThrow("Unknown layout control");
expect(() => snapshotToolcraftModuleData({ callback: () => { } })).toThrow("serializable data");
let reads = 0;
expect(() => snapshotToolcraftModuleData({ get value() { reads += 1; return 1; } })).toThrow("accessors");
expect(reads).toBe(0);
});
it("binds a new panel, applies its typed command once and restores its payload through the shared codec", () => {
const definition = notesModule();
const resolution = resolveToolcraftModuleDependencies([definition], []);
expect(resolveToolcraftPanelSurfaceContributions(definition.contributions)).toEqual({ notes: { enabled: true } });
const state = reduceNotes({ notes: [], revision: 0 }, { type: "notes.replace", notes: ["First", "Second"] });
expect(state.revision).toBe(1);
const saved = JSON.stringify(notesPersistence.write(state, undefined, new Set(["notes"])));
const restored = notesPersistence.read(undefined, JSON.parse(saved), new Set(["notes"]));
expect(restored).toEqual({ notes: ["First", "Second"] });
expect(notesPersistence.read(undefined, { notes: [12] }, new Set(["notes"]))).toBeUndefined();
expect(notesPersistence.write(state, undefined, new Set())).toEqual({});
const bindings = resolveToolcraftModuleBindings(resolution.modules, { notes: NotesSurface });
const view = render(<>{bindings.map(({ moduleId, binding: Surface }) => )}>);
expect(screen.getByLabelText("Notes").textContent).toBe("First; Second");
expect(view.container.querySelector('[data-panel-id="notes"]')).not.toBeNull();
const absent = resolveToolcraftModuleBindings([], { notes: NotesSurface });
view.rerender(<>{absent.map(({ moduleId, binding: Surface }) => )}>);
expect(screen.queryByLabelText("Notes")).toBeNull();
});
it("rejects command and persistence ownership collisions instead of overwriting a handler", () => {
expect(() => assertUniqueToolcraftCommandOwners({ notes: notesCommands, duplicate: notesCommands })).toThrow('command "notes.replace" has multiple owners');
const codec = { fields: ["notes"] as const, read: () => undefined, write: () => ({ notes: [] }) };
expect(() => createToolcraftPersistenceCodec({ first: codec, second: codec })).toThrow('Persistence field "notes" has multiple owners');
// @ts-expect-error A notes module requires a matching executable entry in its catalog.
expect(() => resolveToolcraftModuleBindings([{ id: "notes" }], {})).toThrow('Missing runtime binding');
});
});