/**
 * Shared test-double factories for SmartAgent integration testing.
 *
 * Consumers can import from '@mcp-abap-adt/llm-agent/testing' to build
 * deterministic stubs for all SmartAgent interfaces without duplicating
 * factory code.
 *
 * @example
 * ```typescript
 * import { makeLlm, makeRag, makeDefaultDeps } from '@mcp-abap-adt/llm-agent/testing';
 * ```
 */

import type {
  IContextAssembler,
  ILlm,
  ILogger,
  IMcpClient,
  IQueryEmbedding,
  IRag,
  IRagProviderRegistry,
  IRagRegistry,
  ISubpromptClassifier,
  IToolCache,
  LogEvent,
  Message,
} from '@mcp-abap-adt/llm-agent';
import {
  AssemblerError,
  type CallOptions,
  CircuitBreaker,
  type CircuitBreakerConfig,
  ClassifierError,
  type IQueryExpander,
  LlmError,
  type LlmFinishReason,
  type LlmResponse,
  type LlmStreamChunk,
  type LlmToolCall,
  McpError,
  type McpTool,
  type McpToolResult,
  NoopQueryExpander,
  NoopToolCache,
  RagError,
  type RagMetadata,
  type RagResult,
  type Result,
  type Subprompt,
  ToolCache,
} from '@mcp-abap-adt/llm-agent';
import type { SmartAgent } from '../agent.js';
import type { IMcpConnectionStrategy } from '../interfaces/mcp-connection-strategy.js';
import { InMemoryMetrics } from '../metrics/in-memory-metrics.js';
import type { IMetrics } from '../metrics/types.js';
import type { IPromptInjectionDetector, IToolPolicy } from '../policy/types.js';
import { NoopReranker } from '../reranker/noop-reranker.js';
import type { IReranker } from '../reranker/types.js';
import { NoopSessionManager } from '../session/noop-session-manager.js';
import { SessionManager } from '../session/session-manager.js';
import type { ISessionManager } from '../session/types.js';
import type { ISpan, ITracer, SpanStatus } from '../tracer/types.js';
import { NoopValidator } from '../validator/noop-validator.js';
import type { IOutputValidator } from '../validator/types.js';

// ---------------------------------------------------------------------------
// LLM stub
// ---------------------------------------------------------------------------

export function makeLlm(
  responses: Array<
    | {
        content: string;
        toolCalls?: LlmToolCall[];
        finishReason?: LlmFinishReason;
      }
    | Error
  >,
): ILlm & { callCount: number } {
  let callCount = 0;
  const queue = [...responses];
  return {
    get callCount() {
      return callCount;
    },
    async chat(): Promise<Result<LlmResponse, LlmError>> {
      callCount++;
      const next = queue.shift();
      if (!next) {
        return {
          ok: true,
          value: { content: 'default', finishReason: 'stop' },
        };
      }
      if (next instanceof Error) {
        return { ok: false, error: new LlmError(next.message) };
      }
      return {
        ok: true,
        value: {
          content: next.content,
          toolCalls: next.toolCalls,
          finishReason: next.finishReason ?? 'stop',
        },
      };
    },
    async *streamChat(): AsyncIterable<Result<LlmStreamChunk, LlmError>> {
      callCount++;
      const next = queue.shift();
      if (!next) {
        yield {
          ok: true,
          value: { content: 'default', finishReason: 'stop' },
        };
        return;
      }
      if (next instanceof Error) {
        yield { ok: false, error: new LlmError(next.message) };
        return;
      }
      yield {
        ok: true,
        value: {
          content: next.content,
          toolCalls: next.toolCalls,
          finishReason: next.finishReason ?? 'stop',
        },
      };
    },
    async healthCheck(): Promise<Result<boolean, LlmError>> {
      const next = queue[0];
      if (next instanceof Error) {
        return { ok: false, error: new LlmError(next.message) };
      }
      return { ok: true, value: true };
    },
  };
}

// ---------------------------------------------------------------------------
// RAG stubs
// ---------------------------------------------------------------------------

export function makeRag(
  queryResults: RagResult[] = [],
): IRag & { upsertCalls: string[] } {
  const upsertCalls: string[] = [];
  const stub: IRag & { upsertCalls: string[] } = {
    upsertCalls,
    async query(
      _embedding: IQueryEmbedding,
    ): Promise<Result<RagResult[], RagError>> {
      return { ok: true, value: queryResults };
    },
    async healthCheck(): Promise<Result<void, RagError>> {
      return { ok: true, value: undefined };
    },
    async getById(_id: string): Promise<Result<RagResult | null, RagError>> {
      return { ok: true, value: null };
    },
    writer() {
      return {
        upsertRaw: async (
          _id: string,
          text: string,
        ): Promise<Result<void, RagError>> => {
          upsertCalls.push(text);
          return { ok: true, value: undefined };
        },
        deleteByIdRaw: async (
          _id: string,
        ): Promise<Result<boolean, RagError>> => {
          return { ok: true, value: false };
        },
      };
    },
  };
  return stub;
}

export function makeFailingRag(): IRag & { upsertCalls: string[] } {
  const upsertCalls: string[] = [];
  const stub: IRag & { upsertCalls: string[] } = {
    upsertCalls,
    async query(
      _embedding: IQueryEmbedding,
    ): Promise<Result<RagResult[], RagError>> {
      return { ok: false, error: new RagError('Query failed') };
    },
    async healthCheck(): Promise<Result<void, RagError>> {
      return { ok: false, error: new RagError('Health check failed') };
    },
    async getById(_id: string): Promise<Result<RagResult | null, RagError>> {
      return { ok: false, error: new RagError('getById failed') };
    },
  };
  return stub;
}

/** RAG stub that records metadata passed to writer().upsertRaw (for session-policy tests). */
export function makeMetadataRag(queryResults: RagResult[] = []): IRag & {
  upsertCalls: string[];
  upsertMetadata: RagMetadata[];
  queryCalls: Array<{ text: string; k: number }>;
} {
  const upsertCalls: string[] = [];
  const upsertMetadata: RagMetadata[] = [];
  const queryCalls: Array<{ text: string; k: number }> = [];
  const stub: IRag & {
    upsertCalls: string[];
    upsertMetadata: RagMetadata[];
    queryCalls: Array<{ text: string; k: number }>;
  } = {
    upsertCalls,
    upsertMetadata,
    queryCalls,
    async query(
      embedding: IQueryEmbedding,
      k: number,
    ): Promise<Result<RagResult[], RagError>> {
      queryCalls.push({ text: embedding.text, k });
      return { ok: true, value: queryResults };
    },
    async healthCheck(): Promise<Result<void, RagError>> {
      return { ok: true, value: undefined };
    },
    async getById(_id: string): Promise<Result<RagResult | null, RagError>> {
      return { ok: true, value: null };
    },
    writer() {
      return {
        upsertRaw: async (
          id: string,
          text: string,
          metadata: RagMetadata,
        ): Promise<Result<void, RagError>> => {
          upsertCalls.push(text);
          upsertMetadata.push({ ...metadata, id });
          return { ok: true, value: undefined };
        },
        deleteByIdRaw: async (
          _id: string,
        ): Promise<Result<boolean, RagError>> => {
          return { ok: true, value: false };
        },
      };
    },
  };
  return stub;
}

// ---------------------------------------------------------------------------
// MCP client stub
// ---------------------------------------------------------------------------

export function makeMcpClient(
  tools: McpTool[],
  callResults?: Map<string, McpToolResult | Error>,
): IMcpClient & { callCount: number } {
  let callCount = 0;
  return {
    get callCount() {
      return callCount;
    },
    async listTools(): Promise<Result<McpTool[], McpError>> {
      return { ok: true, value: tools };
    },
    async callTool(name: string): Promise<Result<McpToolResult, McpError>> {
      callCount++;
      const result = callResults?.get(name);
      if (result instanceof Error) {
        return { ok: false, error: new McpError(result.message) };
      }
      if (result) {
        return { ok: true, value: result };
      }
      return { ok: true, value: { content: `result of ${name}` } };
    },
  };
}

// ---------------------------------------------------------------------------
// Classifier stub
// ---------------------------------------------------------------------------

export function makeClassifier(
  result: Subprompt[] | Error,
  onCall?: () => void,
): ISubpromptClassifier {
  return {
    async classify(): Promise<Result<Subprompt[], ClassifierError>> {
      onCall?.();
      if (result instanceof Error) {
        const code =
          result.message === 'ABORTED' ? 'ABORTED' : 'CLASSIFIER_ERROR';
        return { ok: false, error: new ClassifierError(result.message, code) };
      }
      return { ok: true, value: result };
    },
  };
}

// ---------------------------------------------------------------------------
// Assembler stub
// ---------------------------------------------------------------------------

export function makeAssembler(result?: Message[] | Error): IContextAssembler {
  const defaultMessages: Message[] = [{ role: 'user', content: 'action text' }];
  return {
    async assemble(
      _action: Subprompt,
      _retrieved: {
        ragResults: Record<string, RagResult[]>;
        tools: McpTool[];
      },
      _history: Message[],
      _opts?: CallOptions,
    ): Promise<Result<Message[], AssemblerError>> {
      const r = result ?? defaultMessages;
      if (r instanceof Error) {
        const code = r.message === 'ABORTED' ? 'ABORTED' : 'ASSEMBLER_ERROR';
        return { ok: false, error: new AssemblerError(r.message, code) };
      }
      return { ok: true, value: r };
    },
  };
}

// ---------------------------------------------------------------------------
// Capturing logger
// ---------------------------------------------------------------------------

export function makeCapturingLogger(): ILogger & { events: LogEvent[] } {
  const events: LogEvent[] = [];
  return {
    events,
    log(event: LogEvent): void {
      events.push(event);
    },
  };
}

// ---------------------------------------------------------------------------
// Capturing tracer
// ---------------------------------------------------------------------------

export interface CapturedSpan {
  name: string;
  parentName?: string;
  attributes: Record<string, string | number | boolean>;
  events: Array<{
    name: string;
    attributes?: Record<string, string | number | boolean>;
  }>;
  status?: { status: SpanStatus; message?: string };
  ended: boolean;
}

export function makeCapturingTracer(): ITracer & { spans: CapturedSpan[] } {
  const spans: CapturedSpan[] = [];
  return {
    spans,
    startSpan(
      name: string,
      options?: {
        parent?: ISpan;
        attributes?: Record<string, string | number | boolean>;
        traceId?: string;
      },
    ): ISpan {
      const captured: CapturedSpan = {
        name,
        parentName: options?.parent?.name,
        attributes: { ...options?.attributes },
        events: [],
        ended: false,
      };
      if (options?.traceId) {
        captured.attributes['smart_agent.trace_id'] = options.traceId;
      }
      spans.push(captured);
      return {
        get name() {
          return captured.name;
        },
        setAttribute(key: string, value: string | number | boolean): void {
          captured.attributes[key] = value;
        },
        addEvent(
          eventName: string,
          attributes?: Record<string, string | number | boolean>,
        ): void {
          captured.events.push({ name: eventName, attributes });
        },
        setStatus(status: SpanStatus, message?: string): void {
          captured.status = { status, message };
        },
        end(): void {
          captured.ended = true;
        },
      };
    },
  };
}

// ---------------------------------------------------------------------------
// Capturing metrics
// ---------------------------------------------------------------------------

/** Returns an InMemoryMetrics instance for test assertions. */
export function makeCapturingMetrics(): InMemoryMetrics {
  return new InMemoryMetrics();
}

// ---------------------------------------------------------------------------
// Circuit breaker factory
// ---------------------------------------------------------------------------

/** Returns a CircuitBreaker for testing. */
export function makeCircuitBreaker(
  config?: CircuitBreakerConfig,
): CircuitBreaker {
  return new CircuitBreaker(config);
}

// ---------------------------------------------------------------------------
// Reranker stub
// ---------------------------------------------------------------------------

/** Returns a NoopReranker (pass-through) or a custom IReranker for testing. */
export function makeReranker(custom?: IReranker): IReranker {
  return custom ?? new NoopReranker();
}

// ---------------------------------------------------------------------------
// Query expander stub
// ---------------------------------------------------------------------------

/** Returns a NoopQueryExpander (pass-through) or a custom IQueryExpander for testing. */
export function makeQueryExpander(custom?: IQueryExpander): IQueryExpander {
  return custom ?? new NoopQueryExpander();
}

// ---------------------------------------------------------------------------
// Tool cache stub
// ---------------------------------------------------------------------------

/** Returns a ToolCache or NoopToolCache for testing. */
export function makeToolCache(
  opts?: { ttlMs?: number } | IToolCache,
): IToolCache {
  if (opts && 'get' in opts) return opts;
  return opts ? new ToolCache(opts) : new NoopToolCache();
}

// ---------------------------------------------------------------------------
// Output validator stub
// ---------------------------------------------------------------------------

/** Returns a NoopValidator (pass-through) or a custom IOutputValidator for testing. */
export function makeOutputValidator(
  custom?: IOutputValidator,
): IOutputValidator {
  return custom ?? new NoopValidator();
}

// ---------------------------------------------------------------------------
// Session manager stub
// ---------------------------------------------------------------------------

/** Returns a SessionManager or NoopSessionManager for testing. */
export function makeSessionManager(
  opts?: { tokenBudget?: number } | ISessionManager,
): ISessionManager {
  if (opts && 'addTokens' in opts) return opts;
  return opts?.tokenBudget
    ? new SessionManager({ tokenBudget: opts.tokenBudget })
    : new NoopSessionManager();
}

// ---------------------------------------------------------------------------
// Default deps factory
// ---------------------------------------------------------------------------

export function makeDefaultDeps(overrides?: {
  llmResponses?: Array<
    | {
        content: string;
        toolCalls?: LlmToolCall[];
        finishReason?: LlmFinishReason;
      }
    | Error
  >;
  classifier?: ISubpromptClassifier;
  assembler?: IContextAssembler;
  mcpClients?: IMcpClient[];
  ragStores?: Record<string, IRag>;
  ragRegistry?: IRagRegistry;
  ragProviderRegistry?: IRagProviderRegistry;
  reranker?: IReranker;
  queryExpander?: IQueryExpander;
  logger?: ILogger;
  toolPolicy?: IToolPolicy;
  injectionDetector?: IPromptInjectionDetector;
  toolCache?: IToolCache;
  outputValidator?: IOutputValidator;
  sessionManager?: ISessionManager;
  tracer?: ITracer;
  metrics?: IMetrics;
  connectionStrategy?: IMcpConnectionStrategy;
}): {
  llm: ILlm & { callCount: number };
  deps: ConstructorParameters<typeof SmartAgent>[0];
} {
  const llm = makeLlm(
    overrides?.llmResponses ?? [{ content: 'hello', finishReason: 'stop' }],
  );
  return {
    llm,
    deps: {
      mainLlm: llm,
      mcpClients: overrides?.mcpClients ?? [],
      ragStores: overrides?.ragStores ?? {},
      ragRegistry: overrides?.ragRegistry,
      ragProviderRegistry: overrides?.ragProviderRegistry,
      classifier:
        overrides?.classifier ??
        makeClassifier([{ type: 'action', text: 'do something' }]),
      assembler: overrides?.assembler ?? makeAssembler(),
      reranker: overrides?.reranker,
      queryExpander: overrides?.queryExpander,
      logger: overrides?.logger,
      toolPolicy: overrides?.toolPolicy,
      injectionDetector: overrides?.injectionDetector,
      toolCache: overrides?.toolCache,
      outputValidator: overrides?.outputValidator,
      sessionManager: overrides?.sessionManager,
      tracer: overrides?.tracer,
      metrics: overrides?.metrics,
      connectionStrategy: overrides?.connectionStrategy,
    },
  };
}
