import { describe, it, expect, vi } from "vitest";
import "reflect-metadata";
import { Container } from "inversify";
import { wireUseCase } from "@/conformance/wire-use-case";
import {
  isInstrumented,
  isCaptured,
  isAudited,
  isAnalyzed,
  isRateLimited,
} from "@/conformance/brand-runtime";
import type {
  ITracer,
  ISpan,
  SpanOpts,
} from "@/instrumentation/tracer.interface";
import type { ILogger } from "@/instrumentation/logger.interface";
import type { AuditLogProtocol, AnalyticsProtocol } from "@/di/bind-protocols";
import { NoopRateLimit } from "@/rate-limit/noop-rate-limit";

function makeTracer() {
  const calls: SpanOpts[] = [];
  const tracer: ITracer = {
    startSpan: vi.fn(async (opts, fn) => {
      calls.push(opts);
      const span: ISpan = { setAttribute: () => {}, setStatus: () => {} };
      return fn(span);
    }),
  };
  return { tracer, calls };
}

function makeLogger(): ILogger & {
  captureException: ReturnType<typeof vi.fn>;
} {
  return {
    captureException: vi.fn(),
    captureMessage: vi.fn(),
    addBreadcrumb: vi.fn(),
    setUser: vi.fn(),
  };
}

function makeAuditLog(): AuditLogProtocol {
  return { record: vi.fn() };
}

function makeAnalytics(): AnalyticsProtocol {
  return { track: vi.fn() };
}

const doubleFactory = () => async (x: number) => x * 2;

describe("wireUseCase — no-audit path", () => {
  it("attaches Instrumented + Captured brands, no Audited brand", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
    expect(isAudited(wired)).toBe(false);
  });

  it("executes the factory result on invocation", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    await expect(wired(3)).resolves.toBe(6);
  });
});

describe("wireUseCase — audit path", () => {
  it("attaches Instrumented + Captured + Audited brands when auditLog is provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const auditLog = makeAuditLog();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      auditLog,
    });

    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
    expect(isAudited(wired)).toBe(true);
  });

  it("executes the factory result on invocation (audit path)", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const auditLog = makeAuditLog();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      auditLog,
    });

    await expect(wired(5)).resolves.toBe(10);
  });
});

describe("wireUseCase — span-name derivation", () => {
  it("derives span name as <feature>.<name> and uses layer as op", async () => {
    const { tracer, calls } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("auth.signIn");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "auth",
      layer: "use-case",
      name: "signIn",
      tracer,
      logger,
    });

    await wired(1);
    expect(calls).toHaveLength(1);
    expect(calls[0]).toMatchObject({ name: "auth.signIn", op: "use-case" });
  });
});

describe("wireUseCase — capture-tag structure", () => {
  it("captures with { feature, layer, name: '<feature>.<name>' } when an error is thrown", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("blog.getArticles");
    const err = new Error("boom");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: () => async () => {
        throw err;
      },
      deps: [],
      feature: "blog",
      layer: "use-case",
      name: "getArticles",
      tracer,
      logger,
    });

    await expect(wired()).rejects.toBe(err);
    expect(logger.captureException).toHaveBeenCalledWith(err, {
      tags: { feature: "blog", layer: "use-case", name: "blog.getArticles" },
    });
  });
});

describe("wireUseCase — container binding", () => {
  it("binds the wired value to the container symbol", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    expect(container.get(sym)).toBe(wired);
  });

  it("passes deps tuple to the factory", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.scale");

    const scaleFactory = (multiplier: number) => async (x: number) =>
      x * multiplier;

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: scaleFactory,
      deps: [3] as [number],
      feature: "test",
      layer: "use-case",
      name: "scale",
      tracer,
      logger,
    });

    await expect(wired(4)).resolves.toBe(12);
  });
});

describe("wireUseCase — analytics path", () => {
  it("attaches Analyzed brand when analytics is provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const analytics = makeAnalytics();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      analytics,
    });

    expect(isAnalyzed(wired)).toBe(true);
    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
  });

  it("attaches Analyzed + Audited brands when both analytics and auditLog are provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const analytics = makeAnalytics();
    const auditLog = makeAuditLog();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      analytics,
      auditLog,
    });

    expect(isAnalyzed(wired)).toBe(true);
    expect(isAudited(wired)).toBe(true);
    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
  });

  it("executes the factory result on invocation (analytics path)", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const analytics = makeAnalytics();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      analytics,
    });

    await expect(wired(7)).resolves.toBe(14);
  });
});

describe("wireUseCase — no-analytics path", () => {
  it("does not attach Analyzed brand when analytics is absent", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    expect(isAnalyzed(wired)).toBe(false);
  });
});

describe("wireUseCase — idempotent re-bind", () => {
  it("unbinds the existing binding and replaces it when the symbol is already bound", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    const second = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    expect(container.get(sym)).toBe(second);
  });
});

describe("wireUseCase — rate limit path", () => {
  it("attaches RateLimited brand when rateLimit is provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const rateLimit = new NoopRateLimit();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      rateLimit,
    });

    expect(isRateLimited(wired)).toBe(true);
    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
  });

  it("does not attach RateLimited brand when rateLimit is absent", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
    });

    expect(isRateLimited(wired)).toBe(false);
  });

  it("executes factory correctly when rateLimit is provided", async () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const rateLimit = new NoopRateLimit();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      rateLimit,
    });

    await expect(wired(5)).resolves.toBe(10);
  });

  it("attaches RateLimited + Analyzed brands when both rateLimit and analytics are provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const rateLimit = new NoopRateLimit();
    const analytics = makeAnalytics();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      rateLimit,
      analytics,
    });

    expect(isRateLimited(wired)).toBe(true);
    expect(isAnalyzed(wired)).toBe(true);
    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
  });

  it("attaches RateLimited + Audited brands when both rateLimit and auditLog are provided", () => {
    const { tracer } = makeTracer();
    const logger = makeLogger();
    const container = new Container();
    const sym = Symbol("test.double");
    const rateLimit = new NoopRateLimit();
    const auditLog = makeAuditLog();

    const wired = wireUseCase({
      container,
      symbol: sym,
      factory: doubleFactory,
      deps: [],
      feature: "test",
      layer: "use-case",
      name: "double",
      tracer,
      logger,
      rateLimit,
      auditLog,
    });

    expect(isRateLimited(wired)).toBe(true);
    expect(isAudited(wired)).toBe(true);
    expect(isInstrumented(wired)).toBe(true);
    expect(isCaptured(wired)).toBe(true);
  });
});
