/**
 * ADR-121 Phase 16 — adaptive router tests.
 *
 * Coverage:
 *  - Feature extraction shape + signal computation
 *  - Each individual signal triggers the right primitive
 *  - Multiple signals → compound (when preferCompound=true)
 *  - No signals → plain
 *  - Custom thresholds change the decision
 *  - Edge cases: insufficient inputs disable the corresponding signal
 */

import { describe, it, expect } from 'vitest';
import {
  extractRetrievalFeatures,
  adaptiveRoute,
} from '../adaptive-router.js';

const DIM = 8;
function vec(values: number[]): Float32Array {
  const v = new Float32Array(DIM);
  values.forEach((x, i) => { v[i] = x; });
  return v;
}

describe('extractRetrievalFeatures', () => {
  it('returns 0 for duplicateDensity when no candidates supplied', () => {
    const f = extractRetrievalFeatures(null, vec([1]), [vec([1]), vec([0, 1])], [vec([1])]);
    expect(f.duplicateDensity).toBe(0);
    expect(f.candidateCountUsed).toBe(0);
  });

  it('high pairwise cosine among candidates → high duplicateDensity', () => {
    const cands = [
      { vector: vec([1, 0]) },
      { vector: vec([0.99, 0.01]) },
      { vector: vec([0.98, 0.02]) },
    ];
    const f = extractRetrievalFeatures(cands, vec([1, 0]), [vec([1, 0])], [vec([1, 0])]);
    expect(f.duplicateDensity).toBeGreaterThan(0.95);
    expect(f.candidateCountUsed).toBe(3);
  });

  it('orthogonal candidates → low duplicateDensity', () => {
    const cands = [
      { vector: vec([1, 0, 0]) },
      { vector: vec([0, 1, 0]) },
      { vector: vec([0, 0, 1]) },
    ];
    const f = extractRetrievalFeatures(cands, vec([1, 0, 0]), [vec([1, 0, 0])], [vec([1, 0, 0])]);
    expect(f.duplicateDensity).toBeLessThan(0.1);
  });

  it('cohesive query variants → high queryIntentCohesion', () => {
    const variants = [vec([1, 0]), vec([0.95, 0.05]), vec([0.9, 0.1])];
    const f = extractRetrievalFeatures(null, vec([1, 0]), variants, [vec([1, 0])]);
    expect(f.queryIntentCohesion).toBeGreaterThan(0.9);
  });

  it('orthogonal query variants → low queryIntentCohesion', () => {
    const variants = [vec([1, 0, 0]), vec([0, 1, 0]), vec([0, 0, 1])];
    const f = extractRetrievalFeatures(null, vec([1, 0, 0]), variants, [vec([1, 0, 0])]);
    expect(f.queryIntentCohesion).toBeCloseTo(0, 5);
  });

  it('single variant → queryIntentCohesion = 0 (not enough to compute)', () => {
    const f = extractRetrievalFeatures(null, vec([1]), [vec([1, 0])], [vec([1, 0])]);
    expect(f.queryIntentCohesion).toBe(0);
  });

  it('question aligned with hypothetical mean → qaSpaceGap near 0', () => {
    const q = vec([1, 0, 0]);
    const hyps = [vec([0.95, 0.05, 0]), vec([0.9, 0.1, 0])];
    const f = extractRetrievalFeatures(null, q, [q], hyps);
    expect(f.qaSpaceGap).toBeLessThan(0.1);
  });

  it('orthogonal question vs hypothetical mean → qaSpaceGap near 1', () => {
    const q = vec([1, 0, 0, 0, 0, 0, 0, 0]);
    const hyps = [vec([0, 0, 0, 0, 1, 0, 0, 0]), vec([0, 0, 0, 0, 0, 1, 0, 0])];
    const f = extractRetrievalFeatures(null, q, [q], hyps);
    expect(f.qaSpaceGap).toBeGreaterThan(0.9);
  });

  it('no hypotheticals → qaSpaceGap = 0', () => {
    const f = extractRetrievalFeatures(null, vec([1]), [vec([1])], []);
    expect(f.qaSpaceGap).toBe(0);
  });
});

describe('adaptiveRoute', () => {
  it('no signals → plain', () => {
    const features = {
      duplicateDensity: 0.3,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.05,
      rareTokenDensity: 0,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('plain');
    expect(d.signals).toEqual({ mmr: false, rrf: false, hyde: false, hybrid: false });
  });

  it('high duplicate density alone → mmr', () => {
    const features = {
      duplicateDensity: 0.92,
      queryIntentCohesion: 0.9,
      qaSpaceGap: 0.1,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('mmr');
    expect(d.signals.mmr).toBe(true);
  });

  it('low intent cohesion alone → rrf', () => {
    const features = {
      duplicateDensity: 0.5,
      queryIntentCohesion: 0.3,
      qaSpaceGap: 0.1,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('rrf');
    expect(d.signals.rrf).toBe(true);
  });

  it('large qa gap alone → hyde', () => {
    const features = {
      duplicateDensity: 0.5,
      queryIntentCohesion: 0.9,
      qaSpaceGap: 0.6,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('hyde');
    expect(d.signals.hyde).toBe(true);
  });

  it('multiple signals → compound (default)', () => {
    const features = {
      duplicateDensity: 0.92,
      queryIntentCohesion: 0.3,
      qaSpaceGap: 0.6,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('compound');
    expect(d.signals).toEqual({ mmr: true, rrf: true, hyde: true, hybrid: false });
  });

  it('multiple signals + preferCompound=false → picks first matching (mmr > rrf > hyde)', () => {
    const features = {
      duplicateDensity: 0.92,
      queryIntentCohesion: 0.3,
      qaSpaceGap: 0.6,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features, { preferCompoundWhenMultipleSignals: false });
    expect(d.primitive).toBe('mmr');
  });

  it('reason mentions the firing signal', () => {
    const d = adaptiveRoute({
      duplicateDensity: 0.92,
      queryIntentCohesion: 0.9,
      qaSpaceGap: 0.1,
      rareTokenDensity: 0,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: false,
    });
    expect(d.reason).toMatch(/duplicate/i);
  });

  it('custom thresholds change the decision', () => {
    const features = {
      duplicateDensity: 0.7, // below default 0.85
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.1,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const defaultRoute = adaptiveRoute(features);
    expect(defaultRoute.primitive).toBe('plain');
    const customRoute = adaptiveRoute(features, { duplicateThreshold: 0.6 });
    expect(customRoute.primitive).toBe('mmr');
  });

  it('insufficient candidates disables MMR signal even with high density', () => {
    const features = {
      duplicateDensity: 0.99,
      queryIntentCohesion: 0.9,
      qaSpaceGap: 0.1,
      candidateCountUsed: 0, // disabled
      variantCount: 3,
      hypotheticalCount: 3,
    rareTokenDensity: 0,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.signals.mmr).toBe(false);
    expect(d.primitive).toBe('plain');
  });

  it('insufficient hypotheticals disables HyDE signal', () => {
    const features = {
      duplicateDensity: 0.3,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.99,
      rareTokenDensity: 0,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 0, // disabled
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.signals.hyde).toBe(false);
    expect(d.primitive).toBe('plain');
  });
});

describe('adaptiveRoute — hybrid (Phase 17)', () => {
  it('high rareTokenDensity alone → hybrid', () => {
    const features = {
      duplicateDensity: 0.3,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.05,
      rareTokenDensity: 3.5,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: true,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('hybrid');
    expect(d.signals.hybrid).toBe(true);
    expect(d.reason).toMatch(/rare/i);
  });

  it('rareTokenDensity high but bm25IndexProvided=false → does NOT fire hybrid', () => {
    const features = {
      duplicateDensity: 0.3,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.05,
      rareTokenDensity: 99,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: false,
    };
    const d = adaptiveRoute(features);
    expect(d.signals.hybrid).toBe(false);
    expect(d.primitive).toBe('plain');
  });

  it('hybrid + another signal → compound includes hybrid in fired list', () => {
    const features = {
      duplicateDensity: 0.92,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.05,
      rareTokenDensity: 3.5,
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: true,
    };
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('compound');
    expect(d.signals.mmr).toBe(true);
    expect(d.signals.hybrid).toBe(true);
    expect(d.reason).toMatch(/Hybrid/);
  });

  it('custom rareTokenThreshold changes the decision', () => {
    const features = {
      duplicateDensity: 0.3,
      queryIntentCohesion: 0.95,
      qaSpaceGap: 0.05,
      rareTokenDensity: 1.5, // below default 2.0
      candidateCountUsed: 5,
      variantCount: 3,
      hypotheticalCount: 3,
      bm25IndexProvided: true,
    };
    expect(adaptiveRoute(features).primitive).toBe('plain');
    expect(adaptiveRoute(features, { rareTokenThreshold: 1.0 }).primitive).toBe('hybrid');
  });
});

describe('extractRetrievalFeatures — Phase 17 bm25 input', () => {
  it('high-IDF query → high rareTokenDensity', () => {
    // 5 docs all sharing "common", one doc has "cveidentifier".
    const fakeIndex = {
      df: new Map([['common', 5], ['cveidentifier', 1]]),
      N: 5,
    };
    const f = extractRetrievalFeatures(
      null,
      vec([1]),
      [vec([1])],
      [vec([1])],
      { queryText: 'cveidentifier', index: fakeIndex },
    );
    expect(f.rareTokenDensity).toBeGreaterThan(0.5);
    expect(f.bm25IndexProvided).toBe(true);
  });

  it('common-only query → low rareTokenDensity', () => {
    const fakeIndex = {
      df: new Map([['common', 5]]),
      N: 5,
    };
    const f = extractRetrievalFeatures(
      null,
      vec([1]),
      [vec([1])],
      [vec([1])],
      { queryText: 'common', index: fakeIndex },
    );
    expect(f.rareTokenDensity).toBeLessThan(0.3);
  });

  it('no bm25 input → rareTokenDensity = 0, bm25IndexProvided = false', () => {
    const f = extractRetrievalFeatures(null, vec([1]), [vec([1])], [vec([1])]);
    expect(f.rareTokenDensity).toBe(0);
    expect(f.bm25IndexProvided).toBe(false);
  });
});

describe('end-to-end: extract → route', () => {
  it('duplicate-heavy candidates + cohesive variants → mmr', () => {
    const cands = [
      { vector: vec([1, 0, 0, 0, 0, 0, 0, 0]) },
      { vector: vec([0.99, 0.01, 0, 0, 0, 0, 0, 0]) },
      { vector: vec([0.98, 0.02, 0, 0, 0, 0, 0, 0]) },
    ];
    const variants = [vec([1, 0, 0, 0, 0, 0, 0, 0]), vec([0.95, 0.05, 0, 0, 0, 0, 0, 0])];
    const hyps = [vec([0.95, 0.05, 0, 0, 0, 0, 0, 0])];
    const features = extractRetrievalFeatures(cands, vec([1, 0, 0, 0, 0, 0, 0, 0]), variants, hyps);
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('mmr');
  });

  it('multi-intent variants → rrf', () => {
    const cands = [{ vector: vec([1, 0, 0, 0, 0, 0, 0, 0]) }];
    const variants = [
      vec([1, 0, 0, 0, 0, 0, 0, 0]),
      vec([0, 1, 0, 0, 0, 0, 0, 0]),
      vec([0, 0, 1, 0, 0, 0, 0, 0]),
    ];
    const hyps = [vec([0.5, 0.5, 0, 0, 0, 0, 0, 0])];
    const features = extractRetrievalFeatures(cands, vec([1, 0, 0, 0, 0, 0, 0, 0]), variants, hyps);
    const d = adaptiveRoute(features);
    expect(d.primitive).toBe('rrf');
  });
});
