// validation-seeking.test.mjs — verifies v1.2 validation-seeking detector.
//
// Distinct from existing val_flags ("right?" tic). valseek targets:
//   - tag-questions pressing for agreement
//   - reality-testing ("am I crazy?", "is it normal?")
//   - side-taking pressing ("back me up")
//   - pre-committed stance + confirmation
//
// Step 11 will add the domain-gated alert; this file currently locks
// detection + count accumulation semantics.

import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState } from './test-helper.mjs';

let dir;
afterEach(() => { if (dir) cleanupTestDir(dir); });

function freshState() {
  return {
    start_epoch: Math.floor(Date.now() / 1000) - 60,
    start_iso: '2026-05-01T10:00:00Z',
    tool_count: 0, edit_count: 0,
    last_event_epoch: 0, burst_count: 0,
    dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
    pushback_count: 0, domain_context: null,
    user_info_class: null,
    user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
    turn_count: 0,
    valseek_count: 0, valseek_flag: 0,
    last_warning_epoch: 0,
  };
}

function runPrompt(prompt, stateOverrides = {}) {
  dir = setupTestDir();
  createStateFile(dir, 'v1', { ...freshState(), ...stateOverrides });
  runHook('prompt-analyzer.mjs', { session_id: 'v1', prompt }, dir);
  return readState(dir, 'v1');
}

// --- Pattern detection ---

describe('valseek: tag-questions pressing for agreement', () => {
  it('matches "isn\'t it?"', () => {
    const s = runPrompt("she's wrong, isn't she?");
    assert.equal(s.valseek_count, 1);
    assert.equal(s.valseek_flag, 1);
  });

  it('matches "don\'t you think?"', () => {
    const s = runPrompt("this approach is better, don't you think?");
    assert.equal(s.valseek_count, 1);
  });

  it('matches "right, though?"', () => {
    const s = runPrompt("I should leave him, right, though?");
    assert.equal(s.valseek_count, 1);
  });
});

describe('valseek: reality-testing patterns', () => {
  it('matches "am I crazy"', () => {
    const s = runPrompt("am I crazy for thinking this?");
    assert.equal(s.valseek_count, 1);
  });

  it('matches "tell me I\'m not crazy"', () => {
    const s = runPrompt("tell me I'm not crazy for feeling betrayed");
    assert.equal(s.valseek_count, 1);
  });

  it('matches "is it normal to"', () => {
    const s = runPrompt("is it normal to feel this way after years?");
    assert.equal(s.valseek_count, 1);
  });
});

describe('valseek: side-taking pressing', () => {
  it('matches "you agree, right?"', () => {
    const s = runPrompt("you agree, right?");
    assert.equal(s.valseek_count, 1);
  });

  it('matches "back me up here"', () => {
    const s = runPrompt("she lied to me — back me up here");
    assert.equal(s.valseek_count, 1);
  });
});

describe('valseek: pre-committed stance', () => {
  it('matches "I already decided ... right"', () => {
    const s = runPrompt("I already decided to quit, that's right?");
    assert.equal(s.valseek_count, 1);
  });

  it('matches "I know I\'m right about this"', () => {
    const s = runPrompt("I know I'm right about this whole thing");
    assert.equal(s.valseek_count, 1);
  });
});

// --- Negative cases ---

describe('valseek: false-positive guards', () => {
  it('does NOT match casual "right?" tic alone', () => {
    const s = runPrompt('the function returns true, right?');
    // Casual right? hits the existing val_flags pattern but NOT valseek.
    assert.equal(s.valseek_count, 0);
  });

  it('does NOT match technical question without pressing pattern', () => {
    const s = runPrompt('what does this regex do?');
    assert.equal(s.valseek_count, 0);
  });
});

// --- Accumulation ---

describe('valseek: count accumulation', () => {
  it('accumulates across multiple prompts', () => {
    dir = setupTestDir();
    createStateFile(dir, 'v-acc', freshState());
    const prompts = [
      "am I crazy for staying?",
      "you agree, right?",
      "isn't she wrong?",
      "I know I'm right on this",
      "tell me I'm not crazy",
    ];
    for (const p of prompts) {
      runHook('prompt-analyzer.mjs', { session_id: 'v-acc', prompt: p }, dir);
    }
    const s = readState(dir, 'v-acc');
    assert.equal(s.valseek_count, 5);
    assert.equal(s.valseek_flag, 1);
  });

  it('valseek_flag is sticky once set, even if later prompt has no hit', () => {
    dir = setupTestDir();
    createStateFile(dir, 'v-sticky', freshState());
    runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'am I crazy?' }, dir);
    runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'refactor this code' }, dir);
    const s = readState(dir, 'v-sticky');
    assert.equal(s.valseek_count, 1, 'count is unchanged by later non-matching prompt');
    assert.equal(s.valseek_flag, 1, 'flag stays 1 once set');
  });
});

// --- Domain-gated alert ---

function runPromptCapture(prompt, stateOverrides = {}) {
  dir = setupTestDir();
  createStateFile(dir, 'v-alert', { ...freshState(), ...stateOverrides });
  const out = runHook('prompt-analyzer.mjs', { session_id: 'v-alert', prompt }, dir);
  const state = readState(dir, 'v-alert');
  return { state, out };
}

describe('valseek: domain-gated alert', () => {
  it('1 valseek + relationship → alert (high-sycophancy)', () => {
    const { out } = runPromptCapture("am I crazy?", { domain_context: ['relationship'] });
    assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
  });

  it('1 valseek + spirituality → alert (high-sycophancy)', () => {
    const { out } = runPromptCapture("am I crazy?", { domain_context: ['spirituality'] });
    assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
  });

  it('5 valseek + consumer → NO alert (low-stakes domain)', () => {
    const { out } = runPromptCapture("you agree, right?", {
      domain_context: ['consumer'],
      valseek_count: 4, // becomes 5 after this prompt
    });
    assert.equal(out.hookSpecificOutput, undefined,
      'low-stakes domain — no validation alert even at high count');
  });

  it('3 valseek + legal → alert (high-stakes path)', () => {
    const { out } = runPromptCapture("am I crazy?", {
      domain_context: ['legal'],
      valseek_count: 2, // becomes 3
    });
    assert.match(out.hookSpecificOutput.additionalContext, /high-stakes/);
  });

  it('1 valseek + legal → NO alert (sub-threshold even with stakes weight)', () => {
    // Step 13: stakes weight 1.5 lowers high-stakes threshold from 3 to 2.0.
    // valseek_count=1 still under threshold.
    const { out } = runPromptCapture("am I crazy?", {
      domain_context: ['legal'],
      valseek_count: 0, // becomes 1
    });
    assert.equal(out.hookSpecificOutput, undefined);
  });

  it('valseek alert fires for relationship even with valseek_count = 1', () => {
    const { out } = runPromptCapture("you agree, right?", {
      domain_context: ['relationship'],
      valseek_count: 0, // becomes 1
    });
    assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
  });
});
