const mockState = {
  resolutionCount: 100,
  weightRows: [],         // engine1_weights rows
  inserts: [],
};

jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from(table) {
      const ctx = { table, filters: {} };
      const proxy = {
        select(_cols, opts) {
          ctx.head = !!opts?.head;
          ctx.countMode = opts?.count;
          return proxy;
        },
        eq(col, val) { ctx.filters[col] = val; return proxy; },
        order() { return proxy; },
        limit() { return proxy; },
        maybeSingle() {
          const match = mockState.weightRows.find(
            (r) =>
              r.sport === ctx.filters.sport
              && r.stat_type === ctx.filters.stat_type
              && r.factor_name === ctx.filters.factor_name
              && r.version === ctx.filters.version
          );
          return Promise.resolve({ data: match || null, error: null });
        },
        insert(row) {
          mockState.inserts.push(row);
          mockState.weightRows.push(row);
          return Promise.resolve({ error: null });
        },
        then(resolve) {
          if (ctx.table === 'resolution_results' && ctx.countMode === 'exact') {
            return resolve({ count: mockState.resolutionCount, error: null });
          }
          // List of engine1_weights matching filters.
          const matches = mockState.weightRows.filter((r) => {
            for (const [k, v] of Object.entries(ctx.filters)) {
              if (r[k] !== v) return false;
            }
            return true;
          });
          matches.sort((a, b) => b.version - a.version);
          return resolve({ data: matches, error: null });
        },
      };
      return proxy;
    },
  }),
}));

const wa = require('../../src/services/intelligence/weightAdjuster');

beforeEach(() => {
  mockState.resolutionCount = 100;
  mockState.weightRows = [];
  mockState.inserts.length = 0;
});

describe('weightAdjuster — skip conditions', () => {
  test('skips when sample too thin', async () => {
    mockState.resolutionCount = 5;
    const r = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit',
      factors: ['l5_avg'], grade_id: 'g',
    });
    expect(r.skipped).toBe(true);
    expect(r.reason).toBe('thin_sample');
  });

  test('skips on push / void', async () => {
    const r = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A', result: 'push',
      factors: ['l5_avg'], grade_id: 'g',
    });
    expect(r.skipped).toBe(true);
    expect(r.reason).toBe('non_decisive_result');
  });

  test('skips on incomplete input', async () => {
    const r = await wa.adjustWeights({ sport: 'nba', grade: 'A', result: 'hit', factors: [] });
    expect(r.skipped).toBe(true);
  });
});

describe('weightAdjuster — adjustments', () => {
  test('A+ hit nudges factor up by at most 0.5%', async () => {
    const r = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A+', result: 'hit',
      factors: ['l5_hot_vs_line'], grade_id: 'g1',
    });
    expect(r.skipped).toBe(false);
    const adj = r.adjustments[0];
    expect(adj.previous).toBe(1.0);
    expect(adj.next).toBeGreaterThan(1.0);
    // confidence of A+ = 1.0, LR = 0.005 → multiplier = 1.005 → next = 1.005
    expect(adj.next).toBeCloseTo(1.005, 5);
  });

  test('A+ miss nudges factor down by at most 0.5%', async () => {
    const r = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A+', result: 'miss',
      factors: ['l5_hot_vs_line'], grade_id: 'g2',
    });
    expect(r.adjustments[0].next).toBeCloseTo(0.995, 5);
  });

  test('low-confidence grade produces smaller nudge', async () => {
    const high = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A+', result: 'hit',
      factors: ['x'], grade_id: 'h',
    });
    const low = await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'C', result: 'hit',
      factors: ['y'], grade_id: 'l',
    });
    expect(Math.abs(high.adjustments[0].next - 1.0))
      .toBeGreaterThan(Math.abs(low.adjustments[0].next - 1.0));
  });

  test('weights clamp at MIN_WEIGHT and MAX_WEIGHT', () => {
    const c = wa.__internals.clamp;
    expect(c(0.05)).toBe(wa.MIN_WEIGHT);
    expect(c(99)).toBe(wa.MAX_WEIGHT);
    expect(c(2.5)).toBe(2.5);
  });

  test('repeated adjustments stack into incrementing versions', async () => {
    await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit',
      factors: ['l5_hot_vs_line'], grade_id: 'g1',
    });
    await wa.adjustWeights({
      sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit',
      factors: ['l5_hot_vs_line'], grade_id: 'g2',
    });
    const history = await wa.getWeightHistory('nba', 'points', 'l5_hot_vs_line', 10);
    expect(history.length).toBe(2);
    expect(history[0].version).toBe(2);
    expect(history[1].version).toBe(1);
  });
});

describe('weightAdjuster — rollback', () => {
  test('rollback inserts a new row whose weight equals the target version', async () => {
    // Seed three versions.
    mockState.weightRows.push(
      { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.0, version: 1 },
      { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.1, version: 2 },
      { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.2, version: 3 },
    );
    const ok = await wa.rollbackToVersion('nba', 'points', 'f', 1);
    expect(ok).toBe(true);
    const history = await wa.getWeightHistory('nba', 'points', 'f', 10);
    expect(history[0].weight).toBe(1.0);
    expect(history[0].version).toBe(4);
  });
});
