import { describe, it, expect, vi } from "vitest";
import "reflect-metadata";
import { Container } from "inversify";
import { defineFeature } from "@/conformance/define-feature";
import { ConformanceError } from "@/conformance/conformance-error";
import { assertFeatureConformance } from "@/conformance/assert-bindings";
import { withSpan } from "@/instrumentation/with-span";
import { withCapture } from "@/instrumentation/with-capture";
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
import type { ILogger } from "@/instrumentation/logger.interface";
import type { BindContext } from "@/di/bind-context";

function makeTracer(): ITracer {
  return {
    startSpan: vi.fn(async (_opts, fn) => {
      const span: ISpan = { setAttribute: () => {}, setStatus: () => {} };
      return fn(span);
    }),
  };
}

function makeLogger(): ILogger {
  return {
    captureException: vi.fn(),
    captureMessage: vi.fn(),
    addBreadcrumb: vi.fn(),
    setUser: vi.fn(),
  };
}

function makeCtx(): BindContext {
  return { tracer: makeTracer(), logger: makeLogger() };
}

describe("assertFeatureConformance", () => {
  it("passes when every use case is bound through withSpan + withCapture", () => {
    const container = new Container();
    const sym = Symbol("test.signIn");
    const ctx = makeCtx();
    const bound = withSpan(
      ctx.tracer,
      { name: "test.signIn", op: "use-case" },
      withCapture(ctx.logger, { feature: "test", layer: "use-case" }, async (x: number) => x + 1),
    );
    container.bind(sym).toConstantValue(bound);

    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signIn: { mutates: false, audits: [], publishes: [], consumes: [] },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);

    expect(() =>
      assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
    ).not.toThrow();
  });

  it("throws when a use case binding is missing the __instrumented brand", () => {
    const container = new Container();
    const sym = Symbol("test.signIn");
    const ctx = makeCtx();
    const unwrapped = async (x: number) => x + 1;
    container.bind(sym).toConstantValue(unwrapped);

    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signIn: { mutates: false, audits: [], publishes: [], consumes: [] },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);

    expect(() =>
      assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
    ).toThrow(ConformanceError);
    expect(() =>
      assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
    ).toThrow(/test\.signIn.*__instrumented/);
  });

  it("throws when a use case binding is missing the __captured brand", () => {
    const container = new Container();
    const sym = Symbol("test.signIn");
    const ctx = makeCtx();
    // withSpan-only — no withCapture wrap
    const partial = withSpan(
      ctx.tracer,
      { name: "test.signIn", op: "use-case" },
      async (x: number) => x + 1,
    );
    container.bind(sym).toConstantValue(partial);

    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signIn: { mutates: false, audits: [], publishes: [], consumes: [] },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);

    expect(() =>
      assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
    ).toThrow(/__captured/);
  });

  it("throws when a mutating use case with audits is missing the __audited brand", () => {
    const container = new Container();
    const sym = Symbol("test.signUp");
    const ctx = makeCtx();
    const wrappedNoAudit = withSpan(
      ctx.tracer,
      { name: "test.signUp", op: "use-case" },
      withCapture(ctx.logger, { feature: "test" }, async (x: number) => x + 1),
    );
    container.bind(sym).toConstantValue(wrappedNoAudit);

    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signUp: {
          mutates: true,
          audits: ["user.created"],
          publishes: [],
          consumes: [],
        },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);

    expect(() =>
      assertFeatureConformance(container, manifest, { signUp: sym }, ctx),
    ).toThrow(/__audited/);
  });

  it("passes for a mutating use case with empty audits (no __audited required)", () => {
    const container = new Container();
    const sym = Symbol("test.signUp");
    const ctx = makeCtx();
    const wrappedNoAudit = withSpan(
      ctx.tracer,
      { name: "test.signUp", op: "use-case" },
      withCapture(ctx.logger, { feature: "test" }, async (x: number) => x + 1),
    );
    container.bind(sym).toConstantValue(wrappedNoAudit);

    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signUp: {
          mutates: true,
          audits: [],
          publishes: [],
          consumes: [],
        },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);

    expect(() =>
      assertFeatureConformance(container, manifest, { signUp: sym }, ctx),
    ).not.toThrow();
  });

  it("throws when no symbol is provided for a manifest use case", () => {
    const container = new Container();
    const ctx = makeCtx();
    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signIn: { mutates: false, audits: [], publishes: [], consumes: [] },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);
    expect(() => assertFeatureConformance(container, manifest, {}, ctx)).toThrow(
      /no symbol provided/,
    );
  });

  it("throws when the container cannot resolve the symbol", () => {
    const container = new Container();
    const sym = Symbol("test.signIn");
    const ctx = makeCtx();
    const manifest = defineFeature({
      name: "test",
      requiredCores: [],
      useCases: {
        signIn: { mutates: false, audits: [], publishes: [], consumes: [] },
      },
      realtimeChannels: [],
      jobs: [],
    } as const);
    expect(() =>
      assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
    ).toThrow(ConformanceError);
  });
});
