/**
 * Deterministic test-double adapter.
 *
 * Used by:
 *   - unit tests that don't want a live Foundry call
 *   - DesktopTool integration tests where we want predictable coords
 *   - the bench harness in "dry-run" mode (verify wiring without spend)
 *
 * Behaviour:
 *   - Returns coords from a caller-supplied lookup map keyed by intent.
 *   - If no match, returns a coord derived from a hash of the intent
 *     (stable across runs, useful for property tests).
 *   - Simulates configurable latency + token usage so summarise() output
 *     looks realistic.
 *
 * NEVER ship this in production paths; the registry exposes it under
 * ModelId.MOCK and the prod admin UI hides it.
 *
 * @typedef {import('./types.js').GroundOptions} GroundOptions
 * @typedef {import('./types.js').GroundResult}  GroundResult
 * @typedef {import('./types.js').KnobDef}       KnobDef
 */

import { GroundingModel, makeError } from './GroundingModel.js';
import { ModelId, KnobType, ErrorCode } from './constants.js';

const DEFAULT_LATENCY_MS = 5;
const DEFAULT_TOKENS_PER_CALL = 100;

export class MockAdapter extends GroundingModel {
  static displayName = 'Mock (tests only)';
  static modelId     = ModelId.MOCK;

  /**
   * @param {Object} [cfg]
   * @param {Object<string, {x:number,y:number}>} [cfg.responses]
   *   Map from intent (case-insensitive substring match) to a coord.
   * @param {number} [cfg.latencyMs]   - simulated elapsed_ms
   * @param {number} [cfg.tokens]      - simulated completion_tokens
   * @param {Error}  [cfg.failWith]    - if set, every ground() rejects with this
   */
  constructor(cfg = {}) {
    super();
    this.responses = cfg.responses || {};
    this.latencyMs = Number.isFinite(cfg.latencyMs) ? cfg.latencyMs : DEFAULT_LATENCY_MS;
    this.tokens    = Number.isFinite(cfg.tokens) ? cfg.tokens : DEFAULT_TOKENS_PER_CALL;
    this.failWith  = cfg.failWith || null;
    this.callLog   = []; // tests can introspect calls
  }

  capabilities() {
    return {
      pixelGrounding:        true,
      toolCalling:           true,
      multiTurnLoop:         false,
      visionInput:           true,
      maxImagePixels:        Infinity,
      typicalLatencyMs:      this.latencyMs,
      typicalCostPerCallUSD: 0,
    };
  }

  supportedKnobs() {
    return [
      {
        id: 'force_coords',
        label: 'Force coords (test override)',
        type: KnobType.STRING,
        default: null,
        doc: 'String "x=N, y=N" — overrides lookup map for this call.',
      },
    ];
  }

  /** @param {GroundOptions} opts @returns {Promise<GroundResult>} */
  async ground(opts) {
    if (this.failWith) throw this.failWith;
    if (!opts || typeof opts.intent !== 'string') {
      throw makeError(ErrorCode.INVALID_REQUEST, 'intent is required');
    }
    this.callLog.push({ intent: opts.intent, knobs: opts.knobs });

    // 1) explicit per-call override via knob
    const forced = opts.knobs?.force_coords;
    if (typeof forced === 'string') {
      const m = /x\s*=\s*(-?\d+)\D+y\s*=\s*(-?\d+)/i.exec(forced);
      if (m) return this._buildResult({ x: +m[1], y: +m[2] }, opts);
    }

    // 2) lookup-map match
    const intent = opts.intent.toLowerCase();
    for (const [key, point] of Object.entries(this.responses)) {
      if (intent.includes(key.toLowerCase())) {
        return this._buildResult(point, opts);
      }
    }

    // 3) deterministic fallback (stable hash → image-bounded coord)
    const w = opts.imageSize?.width  || 1920;
    const h = opts.imageSize?.height || 1080;
    const seed = hashString(opts.intent);
    return this._buildResult({ x: seed % w, y: Math.floor(seed / w) % h }, opts);
  }

  _buildResult(coords, opts) {
    return {
      answer: `x=${coords.x}, y=${coords.y}`,
      coords,
      usage: { input_tokens: 0, output_tokens: this.tokens, reasoning_tokens: 0 },
      elapsed_ms: this.latencyMs,
      modelId: MockAdapter.modelId,
    };
  }
}

function hashString(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = ((h << 5) - h) + s.charCodeAt(i);
    h |= 0;
  }
  return Math.abs(h);
}

export default MockAdapter;
