/**
 * In-memory `ObservabilityHook` for tests. Records every span name +
 * attribute mutation so the PII grep test can scrub the recording for
 * forbidden values.
 */

import type { Attributes, AttrValue, ObservabilityHook, Span } from './types.js';

export interface RecordedSpan {
  name: string;
  attributes: Attributes;
  status: 'ok' | 'error' | 'unset';
  statusMessage?: string;
  exceptions: unknown[];
  ended: boolean;
  startedAtMs: number;
  endedAtMs?: number;
}

export interface SpanRecorder extends ObservabilityHook {
  /** All spans started so far (in order). */
  readonly spans: readonly RecordedSpan[];
  /** Drop all recorded spans — convenient between test cases. */
  clear(): void;
  /**
   * Search recorded attributes/values for any of `forbiddenSubstrings`.
   * Returns the offending hits. Use in PII guard tests.
   */
  scanForPII(forbiddenSubstrings: readonly string[]): Array<{
    spanName: string;
    key: string;
    value: AttrValue;
    match: string;
  }>;
}

export function createRecorder(): SpanRecorder {
  const spans: RecordedSpan[] = [];
  const hook: ObservabilityHook = {
    startSpan(name, attrs) {
      const rec: RecordedSpan = {
        name,
        attributes: attrs !== undefined ? { ...attrs } : {},
        status: 'unset',
        exceptions: [],
        ended: false,
        startedAtMs: Date.now(),
      };
      spans.push(rec);
      const span: Span = {
        setAttribute(key, value) {
          if (rec.ended) return;
          rec.attributes[key] = value;
        },
        setAttributes(more) {
          if (rec.ended) return;
          for (const [k, v] of Object.entries(more)) rec.attributes[k] = v;
        },
        recordException(err) {
          if (rec.ended) return;
          rec.exceptions.push(err);
        },
        setStatus(status, message) {
          if (rec.ended) return;
          rec.status = status;
          if (message !== undefined) rec.statusMessage = message;
        },
        end() {
          if (rec.ended) return;
          rec.ended = true;
          rec.endedAtMs = Date.now();
        },
      };
      return span;
    },
  };
  return Object.assign(hook, {
    get spans() {
      return spans as readonly RecordedSpan[];
    },
    clear() {
      spans.length = 0;
    },
    scanForPII(forbidden: readonly string[]) {
      const hits: Array<{ spanName: string; key: string; value: AttrValue; match: string }> = [];
      for (const span of spans) {
        for (const [key, value] of Object.entries(span.attributes)) {
          const haystack = `${key}=${String(value)}`.toLowerCase();
          for (const f of forbidden) {
            if (haystack.includes(f.toLowerCase())) {
              hits.push({ spanName: span.name, key, value, match: f });
            }
          }
        }
      }
      return hits;
    },
  });
}
