import { beforeEach, describe, expect, it } from "vitest";
import { createBus } from "../bus/bus.js";
import type { AuthContract } from "../contracts/auth.js";
import type {
  ConfigAccess,
  EventsEmitter,
  Extension,
  HostAPI,
  Manifest,
  ManifestContributions,
  PermissionDecision,
  PermissionGate,
  PermissionRequest,
  ScheduledJob,
  SecretsAccess,
  StorageNamespace,
} from "../contracts/extension.js";
import { defineEventHook, defineFilter, defineService } from "../contracts/hooks.js";
import type {
  Attributes,
  ErrorAttributes,
  LogDeps,
  Logger,
  LogRecord,
  LogSink,
} from "../contracts/logging.js";
import type { ProviderContract } from "../contracts/provider.js";
import type { ToolContract } from "../contracts/tool.js";
import { createHost, type HostDeps, KERNEL_API_VERSION } from "./host.js";

interface FakeLogger extends Logger {
  readonly logs: Array<{ level: string; message: string; attrs?: Attributes | ErrorAttributes }>;
}

function createFakeLogger(): FakeLogger {
  const logs: Array<{ level: string; message: string; attrs?: Attributes | ErrorAttributes }> = [];
  return {
    logs,
    debug: (message: string, attrs?: Attributes) => {
      if (attrs !== undefined) {
        logs.push({ level: "debug", message, attrs });
      } else {
        logs.push({ level: "debug", message });
      }
    },
    info: (message: string, attrs?: Attributes) => {
      if (attrs !== undefined) {
        logs.push({ level: "info", message, attrs });
      } else {
        logs.push({ level: "info", message });
      }
    },
    warn: (message: string, attrs?: Attributes) => {
      if (attrs !== undefined) {
        logs.push({ level: "warn", message, attrs });
      } else {
        logs.push({ level: "warn", message });
      }
    },
    error: (message: string, attrs?: ErrorAttributes) => {
      if (attrs !== undefined) {
        logs.push({ level: "error", message, attrs });
      } else {
        logs.push({ level: "error", message });
      }
    },
    child(
      _ctx: Partial<import("../contracts/logging.js").LogContext> & { readonly attrs?: Attributes },
    ): Logger {
      return createFakeLogger();
    },
    span(_name: string, _attrs?: Attributes): import("../contracts/logging.js").Span {
      return {
        id: "fake-span",
        log: createFakeLogger(),
        setAttributes() {},
        addLink() {},
        child() {
          return this;
        },
        end() {},
      };
    },
  };
}

function createFakeLogSink(): LogSink & { readonly records: LogRecord[] } {
  const records: LogRecord[] = [];
  return {
    records,
    emit: (record: LogRecord) => {
      records.push(record);
    },
  };
}

function createFakeLogDeps(): LogDeps {
  let idCounter = 0;
  return {
    now: () => 1000 + idCounter * 100,
    newId: () => `span-${++idCounter}`,
  };
}

function createFakeConfig(): ConfigAccess {
  return {
    get: () => undefined,
    getAll: () => ({}),
  };
}

function createFakeStorageFactory(): (ns: string) => StorageNamespace {
  const stores = new Map<string, Map<string, string>>();
  return (ns: string) => {
    let store = stores.get(ns);
    if (!store) {
      store = new Map();
      stores.set(ns, store);
    }
    const s = store;
    return {
      get: async (key: string) => s.get(key) ?? null,
      set: async (key: string, value: string) => {
        s.set(key, value);
      },
      delete: async (key: string) => {
        s.delete(key);
      },
      has: async (key: string) => s.has(key),
      keys: async () => [...s.keys()],
    };
  };
}

function createFakeSecrets(): SecretsAccess {
  const store = new Map<string, string>();
  return {
    get: async (key: string) => store.get(key) ?? null,
    set: async (key: string, value: string) => {
      store.set(key, value);
    },
    delete: async (key: string) => {
      store.delete(key);
    },
  };
}

function createFakePermissions(): PermissionGate {
  return {
    check: async (_request: PermissionRequest): Promise<PermissionDecision> => ({
      allowed: true,
    }),
  };
}

function createFakeScheduler(): {
  readonly register: (job: ScheduledJob) => void;
  readonly jobs: ScheduledJob[];
} {
  const jobs: ScheduledJob[] = [];
  return {
    register: (job: ScheduledJob) => {
      jobs.push(job);
    },
    jobs,
  };
}

function createFakeEvents(): EventsEmitter & { readonly emitted: unknown[] } {
  const emitted: unknown[] = [];
  return {
    emitted,
    emit: (event) => {
      emitted.push(event);
    },
  };
}

function createExtension(
  id: string,
  opts: {
    readonly dependsOn?: readonly string[];
    readonly apiVersion?: string;
    readonly activate?: (host: HostAPI) => void | Promise<void>;
    readonly deactivate?: () => void | Promise<void>;
    readonly contributes?: ManifestContributions;
  } = {},
): Extension {
  const base: Manifest = {
    id,
    name: id,
    version: "1.0.0",
    apiVersion: opts.apiVersion ?? `^${KERNEL_API_VERSION}`,
    trust: "bundled",
  };
  const manifest: Manifest =
    opts.dependsOn !== undefined
      ? { ...base, dependsOn: opts.dependsOn }
      : opts.contributes !== undefined
        ? { ...base, contributes: opts.contributes }
        : base;
  const ext: Extension = {
    manifest,
    activate: opts.activate ?? (() => {}),
  };
  if (opts.deactivate !== undefined) {
    return { ...ext, deactivate: opts.deactivate };
  }
  return ext;
}

function createFakeTool(name: string): ToolContract {
  return {
    name,
    description: `Tool ${name}`,
    parameters: { type: "object" },
    execute: async () => ({ content: "ok" }),
  };
}

function createFakeProvider(id: string): ProviderContract {
  return {
    id,
    stream: async function* () {},
  };
}

function createFakeAuth(id: string): AuthContract {
  return {
    id,
    resolve: async () => null,
  };
}

describe("createHost", () => {
  let logger: FakeLogger;
  let logSink: ReturnType<typeof createFakeLogSink>;
  let logDeps: LogDeps;
  let deps: HostDeps;
  let scheduler: ReturnType<typeof createFakeScheduler>;
  let events: ReturnType<typeof createFakeEvents>;

  beforeEach(() => {
    logger = createFakeLogger();
    logSink = createFakeLogSink();
    logDeps = createFakeLogDeps();
    scheduler = createFakeScheduler();
    events = createFakeEvents();
    deps = {
      logger,
      config: createFakeConfig(),
      storageFactory: createFakeStorageFactory(),
      secrets: createFakeSecrets(),
      permissions: createFakePermissions(),
      scheduler,
      bus: createBus(logger),
      events,
      logSink,
      logDeps,
    };
  });

  describe("activation order", () => {
    it("activates extensions in topological order", async () => {
      const order: string[] = [];

      const a = createExtension("a", {
        activate: () => {
          order.push("a");
        },
      });
      const b = createExtension("b", {
        dependsOn: ["a"],
        activate: () => {
          order.push("b");
        },
      });
      const c = createExtension("c", {
        dependsOn: ["b"],
        activate: () => {
          order.push("c");
        },
      });

      const host = createHost([c, b, a], deps);
      await host.activate();

      expect(order).toEqual(["a", "b", "c"]);
    });

    it("activates independent extensions", async () => {
      const order: string[] = [];

      const a = createExtension("a", {
        activate: () => {
          order.push("a");
        },
      });
      const b = createExtension("b", {
        activate: () => {
          order.push("b");
        },
      });

      const host = createHost([a, b], deps);
      await host.activate();

      expect(order).toHaveLength(2);
      expect(order).toContain("a");
      expect(order).toContain("b");
    });
  });

  describe("fault isolation", () => {
    it("a throwing extension is isolated — others still activate", async () => {
      const order: string[] = [];

      const a = createExtension("a", {
        activate: () => {
          order.push("a");
        },
      });
      const b = createExtension("b", {
        activate: () => {
          throw new Error("boom");
        },
      });
      const c = createExtension("c", {
        activate: () => {
          order.push("c");
        },
      });

      const host = createHost([a, b, c], deps);
      await host.activate();

      expect(order).toEqual(["a", "c"]);
      expect(host.getDisabled()).toHaveLength(1);
      expect(host.getDisabled()[0]?.manifest.id).toBe("b");
      expect(host.getDisabled()[0]?.reason).toContain("boom");
    });

    it("an async-rejecting extension is isolated", async () => {
      const a = createExtension("a", {
        activate: async () => {
          throw new Error("async fail");
        },
      });
      const b = createExtension("b", {
        activate: () => {},
      });

      const host = createHost([a, b], deps);
      await host.activate();

      expect(host.getDisabled()).toHaveLength(1);
      expect(host.getDisabled()[0]?.manifest.id).toBe("a");
    });
  });

  describe("apiVersion compatibility", () => {
    it("activates compatible extensions", async () => {
      const ext = createExtension("good", {
        apiVersion: `^${KERNEL_API_VERSION}`,
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getDisabled()).toHaveLength(0);
    });

    it("disables incompatible extensions without crashing", async () => {
      const ext = createExtension("bad", {
        apiVersion: "^99.0.0",
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getDisabled()).toHaveLength(1);
      expect(host.getDisabled()[0]?.manifest.id).toBe("bad");
      expect(host.getDisabled()[0]?.reason).toContain("incompatible");
    });

    it("logs a warning for disabled extensions", async () => {
      const ext = createExtension("bad", {
        apiVersion: "^99.0.0",
      });

      const host = createHost([ext], deps);
      await host.activate();

      const warnings = logger.logs.filter((l) => l.level === "warn");
      expect(warnings).toHaveLength(1);
      expect(warnings[0]?.message).toContain("bad");
    });
  });

  describe("registries", () => {
    it("defineTool populates the tool registry", async () => {
      const tool = createFakeTool("read-file");
      const ext = createExtension("tools-fs", {
        activate: (host) => {
          host.defineTool(tool);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getTools().size).toBe(1);
      expect(host.getTool("read-file")).toBe(tool);
    });

    it("defineProvider populates the provider registry", async () => {
      const provider = createFakeProvider("anthropic");
      const ext = createExtension("provider-anthropic", {
        activate: (host) => {
          host.defineProvider(provider);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getProviders().size).toBe(1);
      expect(host.getProvider("anthropic")).toBe(provider);
    });

    it("defineAuth populates the auth registry", async () => {
      const auth = createFakeAuth("apikey");
      const ext = createExtension("auth-apikey", {
        activate: (host) => {
          host.defineAuth(auth);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getAuthProviders().size).toBe(1);
      expect(host.getAuthProvider("apikey")).toBe(auth);
    });

    it("getService returns what an extension provided via provideService", async () => {
      const handle = defineService<{ value: number }>("test/svc");
      const ext = createExtension("svc-provider", {
        activate: (host) => {
          host.provideService(handle, { value: 42 });
        },
      });
      const consumer = createExtension("svc-consumer", {
        dependsOn: ["svc-provider"],
        activate: (host) => {
          const svc = host.getService(handle);
          expect(svc.value).toBe(42);
        },
      });

      const host = createHost([ext, consumer], deps);
      await host.activate();

      expect(host.getDisabled()).toHaveLength(0);
    });

    it("multiple extensions contribute to the same registry", async () => {
      const ext1 = createExtension("tools-a", {
        activate: (host) => {
          host.defineTool(createFakeTool("tool-a"));
        },
      });
      const ext2 = createExtension("tools-b", {
        activate: (host) => {
          host.defineTool(createFakeTool("tool-b"));
        },
      });

      const host = createHost([ext1, ext2], deps);
      await host.activate();

      expect(host.getTools().size).toBe(2);
      expect(host.getTool("tool-a")).toBeDefined();
      expect(host.getTool("tool-b")).toBeDefined();
    });
  });

  describe("scheduler", () => {
    it("collects scheduled jobs and forwards to sink", async () => {
      const job: ScheduledJob = {
        id: "cache-warm",
        cron: "*/5 * * * *",
        execute: () => {},
      };
      const ext = createExtension("scheduler-ext", {
        activate: (host) => {
          host.scheduler.register(job);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getScheduledJobs()).toHaveLength(1);
      expect(host.getScheduledJobs()[0]).toBe(job);
      expect(scheduler.jobs).toHaveLength(1);
      expect(scheduler.jobs[0]).toBe(job);
    });
  });

  describe("migrations", () => {
    it("collects migrations from manifests", async () => {
      const ext = createExtension("store-ext", {
        contributes: { migrations: ["001-init", "002-add-index"] },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(host.getMigrations()).toEqual(["001-init", "002-add-index"]);
    });
  });

  describe("deactivation", () => {
    it("deactivates in reverse activation order", async () => {
      const order: string[] = [];

      const a = createExtension("a", {
        activate: () => {
          order.push("activate-a");
        },
        deactivate: () => {
          order.push("deactivate-a");
        },
      });
      const b = createExtension("b", {
        activate: () => {
          order.push("activate-b");
        },
        deactivate: () => {
          order.push("deactivate-b");
        },
      });
      const c = createExtension("c", {
        activate: () => {
          order.push("activate-c");
        },
        deactivate: () => {
          order.push("deactivate-c");
        },
      });

      const host = createHost([a, b, c], deps);
      await host.activate();
      await host.deactivate();

      expect(order).toEqual([
        "activate-a",
        "activate-b",
        "activate-c",
        "deactivate-c",
        "deactivate-b",
        "deactivate-a",
      ]);
    });

    it("a failing deactivate does not prevent others", async () => {
      const order: string[] = [];

      const a = createExtension("a", {
        activate: () => {},
        deactivate: () => {
          order.push("deactivate-a");
        },
      });
      const b = createExtension("b", {
        activate: () => {},
        deactivate: () => {
          throw new Error("deactivate boom");
        },
      });
      const c = createExtension("c", {
        activate: () => {},
        deactivate: () => {
          order.push("deactivate-c");
        },
      });

      const host = createHost([a, b, c], deps);
      await host.activate();
      await host.deactivate();

      expect(order).toEqual(["deactivate-c", "deactivate-a"]);
      const errors = logger.logs.filter((l) => l.level === "error");
      expect(errors.some((e) => e.message.includes("deactivate"))).toBe(true);
      expect(errors.some((e) => (e.attrs as { err?: unknown })?.err instanceof Error)).toBe(true);
    });
  });

  describe("HostAPI delegation", () => {
    it("on/addFilter delegate to the bus", async () => {
      const hook = defineEventHook<string>("test/host-event");
      const received: string[] = [];

      const ext = createExtension("hook-ext", {
        activate: (host) => {
          host.on(hook, (payload) => {
            received.push(payload);
          });
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      deps.bus.emit(hook, "hello");
      expect(received).toEqual(["hello"]);
    });

    it("emit dispatches to handlers registered via on", async () => {
      const hook = defineEventHook<string>("test/emit-dispatch");
      const received: string[] = [];

      const ext = createExtension("emit-ext", {
        activate: (host) => {
          host.on(hook, (payload) => {
            received.push(payload);
          });
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      api.emit(hook, "world");
      expect(received).toEqual(["world"]);
    });

    it("emit isolates a throwing handler (does not propagate)", async () => {
      const hook = defineEventHook<string>("test/emit-isolation");
      const received: string[] = [];

      const ext = createExtension("emit-isolation-ext", {
        activate: (host) => {
          host.on(hook, () => {
            throw new Error("handler boom");
          });
          host.on(hook, (payload) => {
            received.push(payload);
          });
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      expect(() => api.emit(hook, "safe")).not.toThrow();
      expect(received).toEqual(["safe"]);
    });

    it("applyFilters threads a value through registered filters in order", async () => {
      const hook = defineFilter<string>("test/text-transform");

      const ext = createExtension("filter-ext", {
        activate: (host) => {
          host.addFilter(hook, (value) => `${value}-first`);
          host.addFilter(hook, (value) => `${value}-second`);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      const result = await api.applyFilters(hook, "start");
      expect(result).toBe("start-first-second");
    });

    it("applyFilters returns the input unchanged when no filters are registered", async () => {
      const hook = defineFilter<string>("test/unused-filter");

      const ext = createExtension("no-filter-ext", {
        activate: () => {},
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      const result = await api.applyFilters(hook, "unchanged");
      expect(result).toBe("unchanged");
    });

    it("storage delegates to the factory", async () => {
      let storageResult: StorageNamespace | undefined;

      const ext = createExtension("storage-ext", {
        activate: (host) => {
          storageResult = host.storage("my-ns");
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(storageResult).toBeDefined();
      await storageResult?.set("key", "value");
      expect(await storageResult?.get("key")).toBe("value");
    });

    it("events delegates to the emitter", async () => {
      const ext = createExtension("event-ext", {
        activate: (host) => {
          host.events.emit({ type: "custom", data: 42 });
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      expect(events.emitted).toHaveLength(1);
      expect(events.emitted[0]).toEqual({ type: "custom", data: 42 });
    });
  });

  describe("HostAPI registry access", () => {
    it("getTools returns registered tools via HostAPI", async () => {
      const tool = createFakeTool("read-file");
      let capturedTools: ReadonlyMap<string, ToolContract> | undefined;

      const producer = createExtension("tools-fs", {
        activate: (host) => {
          host.defineTool(tool);
        },
      });
      const consumer = createExtension("consumer", {
        dependsOn: ["tools-fs"],
        activate: (host) => {
          capturedTools = host.getTools();
        },
      });

      const host = createHost([producer, consumer], deps);
      await host.activate();

      expect(capturedTools).toBeDefined();
      expect(capturedTools?.size).toBe(1);
      expect(capturedTools?.get("read-file")).toBe(tool);
    });

    it("getProviders returns registered providers via HostAPI", async () => {
      const provider = createFakeProvider("anthropic");
      let capturedProviders: ReadonlyMap<string, ProviderContract> | undefined;

      const producer = createExtension("provider-anthropic", {
        activate: (host) => {
          host.defineProvider(provider);
        },
      });
      const consumer = createExtension("consumer", {
        dependsOn: ["provider-anthropic"],
        activate: (host) => {
          capturedProviders = host.getProviders();
        },
      });

      const host = createHost([producer, consumer], deps);
      await host.activate();

      expect(capturedProviders).toBeDefined();
      expect(capturedProviders?.size).toBe(1);
      expect(capturedProviders?.get("anthropic")).toBe(provider);
    });

    it("getAuthProviders/getAuthProvider returns registered auth via HostAPI", async () => {
      const auth = createFakeAuth("apikey");
      let capturedAuth: ReadonlyMap<string, AuthContract> | undefined;
      let capturedSingle: AuthContract | undefined;

      const producer = createExtension("auth-apikey", {
        activate: (host) => {
          host.defineAuth(auth);
        },
      });
      const consumer = createExtension("consumer", {
        dependsOn: ["auth-apikey"],
        activate: (host) => {
          capturedAuth = host.getAuthProviders();
          capturedSingle = host.getAuthProvider("apikey");
        },
      });

      const host = createHost([producer, consumer], deps);
      await host.activate();

      expect(capturedAuth).toBeDefined();
      expect(capturedAuth?.size).toBe(1);
      expect(capturedAuth?.get("apikey")).toBe(auth);
      expect(capturedSingle).toBe(auth);
    });
  });

  describe("getExtensions", () => {
    it("returns empty array when no extensions are activated", async () => {
      const host = createHost([], deps);
      await host.activate();

      expect(host.getExtensions()).toEqual([]);
    });

    it("returns manifests of all activated extensions", async () => {
      const a = createExtension("ext-a");
      const b = createExtension("ext-b");

      const host = createHost([a, b], deps);
      await host.activate();

      const exts = host.getExtensions();
      expect(exts).toHaveLength(2);
      expect(exts.map((e) => e.id)).toContain("ext-a");
      expect(exts.map((e) => e.id)).toContain("ext-b");
    });

    it("returns manifests in activation order", async () => {
      const a = createExtension("a");
      const b = createExtension("b", { dependsOn: ["a"] });
      const c = createExtension("c", { dependsOn: ["b"] });

      const host = createHost([c, b, a], deps);
      await host.activate();

      const exts = host.getExtensions();
      expect(exts.map((e) => e.id)).toEqual(["a", "b", "c"]);
    });

    it("excludes extensions that failed to activate", async () => {
      const a = createExtension("good");
      const b = createExtension("bad", {
        activate: () => {
          throw new Error("boom");
        },
      });

      const host = createHost([a, b], deps);
      await host.activate();

      const exts = host.getExtensions();
      expect(exts).toHaveLength(1);
      expect(exts[0]?.id).toBe("good");
    });

    it("excludes extensions disabled by apiVersion incompatibility", async () => {
      const good = createExtension("good");
      const bad = createExtension("bad", { apiVersion: "^99.0.0" });

      const host = createHost([good, bad], deps);
      await host.activate();

      const exts = host.getExtensions();
      expect(exts).toHaveLength(1);
      expect(exts[0]?.id).toBe("good");
    });

    it("returns a frozen array", async () => {
      const ext = createExtension("ext");
      const host = createHost([ext], deps);
      await host.activate();

      const exts = host.getExtensions();
      expect(Object.isFrozen(exts)).toBe(true);
    });

    it("HostAPI getExtensions reflects activated extensions after full activation", async () => {
      const a = createExtension("ext-a");
      const b = createExtension("ext-b", {
        dependsOn: ["ext-a"],
        activate: () => {},
      });

      const host = createHost([a, b], deps);
      await host.activate();

      // Use getHostAPI() to verify the post-activation view
      const api = host.getHostAPI();
      const capturedExtsAfter = api.getExtensions();

      expect(capturedExtsAfter).toHaveLength(2);
      expect(capturedExtsAfter.map((e) => e.id)).toEqual(["ext-a", "ext-b"]);
    });

    it("HostAPI getExtensions during activation sees only previously activated", async () => {
      const seenDuringActivation: string[][] = [];

      const a = createExtension("a", {
        activate: (host) => {
          seenDuringActivation.push(host.getExtensions().map((e) => e.id));
        },
      });
      const b = createExtension("b", {
        activate: (host) => {
          seenDuringActivation.push(host.getExtensions().map((e) => e.id));
        },
      });

      const host = createHost([a, b], deps);
      await host.activate();

      // When a activates, activated[] is empty (a hasn't been pushed yet)
      // When b activates, activated[] has [a] (b hasn't been pushed yet)
      expect(seenDuringActivation).toEqual([[], ["a"]]);
    });
  });

  describe("DAG errors", () => {
    it("throws on missing dependency", () => {
      const ext = createExtension("a", { dependsOn: ["missing"] });
      expect(() => createHost([ext], deps)).toThrow(/not available/);
    });

    it("throws on dependency cycle", () => {
      const a = createExtension("a", { dependsOn: ["b"] });
      const b = createExtension("b", { dependsOn: ["a"] });
      expect(() => createHost([a, b], deps)).toThrow(/cycle/i);
    });
  });

  describe("empty host", () => {
    it("works with no extensions", async () => {
      const host = createHost([], deps);
      await host.activate();

      expect(host.getTools().size).toBe(0);
      expect(host.getProviders().size).toBe(0);
      expect(host.getAuthProviders().size).toBe(0);
      expect(host.getScheduledJobs()).toHaveLength(0);
      expect(host.getMigrations()).toHaveLength(0);
      expect(host.getDisabled()).toHaveLength(0);
    });
  });

  describe("getHostAPI", () => {
    it("returns a HostAPI whose read-views reflect registrations from activation", async () => {
      const tool = createFakeTool("read-file");
      const provider = createFakeProvider("anthropic");
      const auth = createFakeAuth("apikey");

      const ext = createExtension("multi-ext", {
        activate: (host) => {
          host.defineTool(tool);
          host.defineProvider(provider);
          host.defineAuth(auth);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();

      expect(api.getTools().size).toBe(1);
      expect(api.getTools().get("read-file")).toBe(tool);

      expect(api.getProviders().size).toBe(1);
      expect(api.getProviders().get("anthropic")).toBe(provider);

      expect(api.getAuthProviders().size).toBe(1);
      expect(api.getAuthProvider("apikey")).toBe(auth);
    });

    it("throws on defineTool after activation", async () => {
      const ext = createExtension("ext", { activate: () => {} });
      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      expect(() => api.defineTool(createFakeTool("late"))).toThrow(
        "Registration not available after activation",
      );
    });

    it("throws on defineProvider after activation", async () => {
      const ext = createExtension("ext", { activate: () => {} });
      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      expect(() => api.defineProvider(createFakeProvider("late"))).toThrow(
        "Registration not available after activation",
      );
    });

    it("throws on defineAuth after activation", async () => {
      const ext = createExtension("ext", { activate: () => {} });
      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      expect(() => api.defineAuth(createFakeAuth("late"))).toThrow(
        "Registration not available after activation",
      );
    });

    it("applyFilters is available on registration-closed HostAPI", async () => {
      const hook = defineFilter<string>("test/closed-filter");

      const ext = createExtension("filter-ext", {
        activate: (host) => {
          host.addFilter(hook, (value) => `${value}-filtered`);
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const api = host.getHostAPI();
      const result = await api.applyFilters(hook, "input");
      expect(result).toBe("input-filtered");
    });
  });

  describe("auto-scoped logger (D6)", () => {
    it("each extension's logger stamps its own manifest.id as extensionId", async () => {
      let extALogger: Logger | undefined;
      let extBLogger: Logger | undefined;

      const a = createExtension("ext-a", {
        activate: (host) => {
          extALogger = host.logger;
        },
      });
      const b = createExtension("ext-b", {
        activate: (host) => {
          extBLogger = host.logger;
        },
      });

      const host = createHost([a, b], deps);
      await host.activate();

      extALogger?.info("from-a");
      extBLogger?.info("from-b");

      const logRecords = logSink.records.filter((r) => r.kind === "log");
      expect(logRecords).toHaveLength(2);
      if (logRecords[0]?.kind === "log") {
        expect(logRecords[0].extensionId).toBe("ext-a");
        expect(logRecords[0].msg).toBe("from-a");
      }
      if (logRecords[1]?.kind === "log") {
        expect(logRecords[1].extensionId).toBe("ext-b");
        expect(logRecords[1].msg).toBe("from-b");
      }
    });

    it("an extension cannot spoof extensionId — it is auto-stamped", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("real-id", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      // child() cannot override extensionId
      const child = extLogger?.child({ extensionId: "spoofed" });
      child?.info("msg");

      const logRecords = logSink.records.filter((r) => r.kind === "log");
      expect(logRecords).toHaveLength(1);
      if (logRecords[0]?.kind === "log") {
        expect(logRecords[0].extensionId).toBe("real-id");
      }
    });

    it("host.logger.error uses structured { err } shape", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      extLogger?.error("something broke", { err: new Error("boom") });

      const logRecords = logSink.records.filter((r) => r.kind === "log");
      expect(logRecords).toHaveLength(1);
      if (logRecords[0]?.kind === "log") {
        expect(logRecords[0].level).toBe("error");
        expect(logRecords[0].msg).toBe("something broke");
        expect(logRecords[0].attributes?.["error.message"]).toBe("boom");
      }
    });

    it("a throwing sink does NOT break the caller", async () => {
      const brokenSink: LogSink = {
        emit() {
          throw new Error("sink down");
        },
      };
      const brokenDeps: HostDeps = {
        ...deps,
        logSink: brokenSink,
      };

      let extLogger: Logger | undefined;
      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], brokenDeps);
      await host.activate();

      // Should not throw
      expect(() => extLogger?.info("msg")).not.toThrow();
    });

    it("span() + end() emit incremental span-open and span-close records", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("my-span", { key: "value" });
      span?.setAttributes({ extra: "attr" });
      span?.end({ attrs: { result: "ok" } });

      const spanOpens = logSink.records.filter((r) => r.kind === "span-open");
      const spanCloses = logSink.records.filter((r) => r.kind === "span-close");

      expect(spanOpens).toHaveLength(1);
      expect(spanCloses).toHaveLength(1);

      if (spanOpens[0]?.kind === "span-open") {
        expect(spanOpens[0].name).toBe("my-span");
        expect(spanOpens[0].extensionId).toBe("ext");
        expect(spanOpens[0].attributes?.key).toBe("value");
      }
      if (spanCloses[0]?.kind === "span-close") {
        expect(spanCloses[0].name).toBe("my-span");
        expect(spanCloses[0].status).toBe("ok");
        expect(spanCloses[0].durationMs).toBeGreaterThanOrEqual(0);
        expect(spanCloses[0].attributes?.extra).toBe("attr");
        expect(spanCloses[0].attributes?.result).toBe("ok");
      }
    });

    it("span() with body emits body on span-open record", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("with-body", { key: "value" }, '{"payload":"hello"}');
      span?.end();

      const spanOpens = logSink.records.filter((r) => r.kind === "span-open");
      expect(spanOpens).toHaveLength(1);
      if (spanOpens[0]?.kind === "span-open") {
        expect(spanOpens[0].body).toBe('{"payload":"hello"}');
      }
    });

    it("span() without body omits body field on span-open record", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("no-body");
      span?.end();

      const spanOpens = logSink.records.filter((r) => r.kind === "span-open");
      expect(spanOpens).toHaveLength(1);
      if (spanOpens[0]?.kind === "span-open") {
        expect(spanOpens[0].body).toBeUndefined();
      }
    });

    it("child() with body emits body on child span-open record", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("parent");
      const child = span?.child("child-name", { k: "v" }, '{"child":"body"}');
      child?.end();
      span?.end();

      const spanOpens = logSink.records.filter((r) => r.kind === "span-open");
      const childOpen = spanOpens.find((r) => r.kind === "span-open" && r.name === "child-name");
      expect(childOpen).toBeDefined();
      if (childOpen?.kind === "span-open") {
        expect(childOpen.body).toBe('{"child":"body"}');
      }
    });

    it("end() with body emits body on span-close record", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("close-body");
      span?.end({ body: '{"result":"data"}' });

      const spanCloses = logSink.records.filter((r) => r.kind === "span-close");
      expect(spanCloses).toHaveLength(1);
      if (spanCloses[0]?.kind === "span-close") {
        expect(spanCloses[0].body).toBe('{"result":"data"}');
      }
    });

    it("end() without body omits body field on span-close record", async () => {
      let extLogger: Logger | undefined;

      const ext = createExtension("ext", {
        activate: (host) => {
          extLogger = host.logger;
        },
      });

      const host = createHost([ext], deps);
      await host.activate();

      const span = extLogger?.span("no-close-body");
      span?.end();

      const spanCloses = logSink.records.filter((r) => r.kind === "span-close");
      expect(spanCloses).toHaveLength(1);
      if (spanCloses[0]?.kind === "span-close") {
        expect(spanCloses[0].body).toBeUndefined();
      }
    });
  });
});
