/**
 * HintEngine unit tests
 * Verifies rule matching, priority ordering, first-match-wins, repetition detection, and logging.
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ActivityTracker } from '../../src/dashboard/activity-tracker';
import { HintEngine } from '../../src/hints/hint-engine';

function makeResult(text: string, isError = false): Record<string, unknown> {
  return {
    content: [{ type: 'text', text: isError ? `Error: ${text}` : text }],
    ...(isError && { isError: true }),
  };
}

function makeTracker(
  calls: Array<{ toolName: string; args?: Record<string, unknown>; result?: 'success' | 'error'; error?: string }> = []
): ActivityTracker {
  const tracker = new ActivityTracker();
  // Seed completed calls (most recent first in getRecentCalls)
  for (const call of [...calls].reverse()) {
    const id = tracker.startCall(call.toolName, 'test', call.args);
    tracker.endCall(id, call.result || 'success', call.error);
  }
  return tracker;
}

describe('HintEngine', () => {
  describe('rule ordering', () => {
    it('should have rules sorted by ascending priority', () => {
      const engine = new HintEngine(new ActivityTracker());
      const rules = engine.getRules();
      for (let i = 1; i < rules.length; i++) {
        expect(rules[i].priority).toBeGreaterThanOrEqual(rules[i - 1].priority);
      }
    });
  });

  describe('first-match-wins', () => {
    it('should return only the first matching hint', () => {
      // "timeout" matches error-recovery AND could match success-hints (navigate error page)
      const tracker = new ActivityTracker();
      const engine = new HintEngine(tracker);
      const result = makeResult('Navigation timeout exceeded', true);
      const hint = engine.getHint('navigate', result, true);
      expect(hint?.hint).toContain('wait_for');
    });

    it('should return null when no rules match', () => {
      const engine = new HintEngine(new ActivityTracker());
      // Use a non-first call to avoid setup-permission-hint (fires once on first success)
      engine.getHint('navigate', makeResult('warmup'), false);
      const result = makeResult('{"action":"navigate","url":"https://example.com","title":"Example"}');
      const hint = engine.getHint('navigate', result, false);
      expect(hint).toBeNull();
    });
  });

  describe('error recovery rules', () => {
    it('should hint on stale ref errors', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc123', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint?.hint).toContain('Refs expire');
      expect(hint?.hint).toContain('read_page');
    });

    it('should hint on tab not found errors', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('tab not found: tab-xyz', true);
      const hint = engine.getHint('navigate', result, true);
      expect(hint?.hint).toContain('tabs_context');
    });

    it('should hint on CSS selector failures', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('selector not found: #my-button', true);
      const hint = engine.getHint('computer', result, true);
      expect(hint?.hint).toContain('find(query)');
    });

    it('should hint on interact "no clickable elements found"', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('No clickable elements found matching "Submit"', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint?.hint).toContain('interact');
      expect(hint?.hint).toContain('read_page');
    });

    it('should hint on interact "no good match found"', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('No good match found for "Login". Best candidate was "Log Out" with low confidence.', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint?.hint).toContain('interact');
    });

    it('should hint on interact generic error', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Click element error: Cannot read properties of null', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint?.hint).toContain('interact');
    });

    it('should hint on timeout errors', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Operation timed out after 30000ms', true);
      const hint = engine.getHint('navigate', result, true);
      expect(hint?.hint).toContain('wait_for');
    });

    it('should hint on null reference errors', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Cannot read property "click" of null', true);
      const hint = engine.getHint('javascript_tool', result, true);
      expect(hint?.hint).toContain('null');
      expect(hint?.hint).toContain('find');
    });

    it('should hint on coordinate-based click attempts', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('click at position requires x, y coordinates', true);
      const hint = engine.getHint('computer', result, true);
      expect(hint?.hint).toContain('interact');
    });

    it('should not trigger error rules for non-error results', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc123', false);
      // Error recovery rules require isError=true
      const hint = engine.getHint('interact', result, false);
      // Should not get error recovery hint
      expect(hint === null || !hint.hint.includes('Refs expire')).toBe(true);
    });
  });

  describe('recovery feedback bundles', () => {
    it('writes a blocked-page feedback bundle when a blocking hint fires', () => {
      const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-feedback-hints-'));
      const engine = new HintEngine(new ActivityTracker());
      engine.enableRecoveryFeedback(tmpDir);

      const hint = engine.getHint(
        'navigate',
        makeResult(JSON.stringify({ blockingPage: { type: 'access-denied', detail: '403 Forbidden' } })),
        false,
        'session-a',
      );

      expect(hint?.rule).toBe('access-denied-detected');
      const files = fs.readdirSync(tmpDir).filter((f) => f.endsWith('.jsonl'));
      expect(files).toHaveLength(1);
      const parsed = JSON.parse(fs.readFileSync(path.join(tmpDir, files[0]), 'utf8').trim());
      expect(parsed.sessionId).toBe('session-a');
      expect(parsed.trigger.category).toBe('blocked_page');
      expect(parsed.hints[0].rule).toBe('access-denied-detected');
    });
  });

  describe('composite suggestion rules', () => {
    it('should suggest interact after find+click pattern', () => {
      const tracker = makeTracker([{ toolName: 'find' }]);
      const engine = new HintEngine(tracker);
      const result = makeResult('clicked element at position');
      const hint = engine.getHint('click', result, false);
      expect(hint?.hint).toContain('interact');
    });

    it('should suggest fill_form after multiple form_input calls', () => {
      const tracker = makeTracker([{ toolName: 'form_input' }]);
      const engine = new HintEngine(tracker);
      const result = makeResult('input filled');
      const hint = engine.getHint('form_input', result, false);
      expect(hint?.hint).toContain('fill_form');
    });

    it('should suggest interact with waitForMs after navigate+click', () => {
      const tracker = makeTracker([{ toolName: 'navigate' }]);
      const engine = new HintEngine(tracker);
      const result = makeResult('clicked');
      const hint = engine.getHint('interact', result, false);
      expect(hint?.hint).toContain('interact');
    });

    it('should suggest find for truncated read_page', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Page content here... truncated at 5000 chars');
      const hint = engine.getHint('read_page', result, false);
      expect(hint?.hint).toContain('find(query)');
    });
  });

  describe('sequence detection rules', () => {
    it('should detect login page after navigate', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('{"action":"navigate","url":"https://app.com/dashboard","title":"App","authRedirect":true}');
      const hint = engine.getHint('navigate', result, false);
      expect(hint?.hint).toContain('login');
      expect(hint?.hint).toContain('Chrome profile');
    });

    it('should detect repeated read_page', () => {
      const tracker = makeTracker([{ toolName: 'read_page' }]);
      const engine = new HintEngine(tracker);
      const result = makeResult('page content...');
      const hint = engine.getHint('read_page', result, false);
      expect(hint?.hint).toContain('find(query)');
    });

    it('should detect navigate→screenshot without wait', () => {
      const tracker = makeTracker([{ toolName: 'navigate' }]);
      const engine = new HintEngine(tracker);
      const result = makeResult('screenshot captured');
      const hint = engine.getHint('computer', result, false);
      expect(hint?.hint).toContain('wait_for');
    });
  });

  describe('success hint rules', () => {
    it('should hint on 404 page after navigate', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('{"action":"navigate","url":"https://example.com/bad","title":"404 Not Found"}');
      const hint = engine.getHint('navigate', result, false);
      expect(hint?.hint).toContain('Verify URL');
    });

    it('should hint when find returns no results', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('0 results found');
      const hint = engine.getHint('find', result, false);
      expect(hint?.hint).toContain('broader query');
    });

    it('should hint after interact with navigation', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Clicked "Submit" button [Page navigated to /dashboard]');
      const hint = engine.getHint('interact', result, false);
      expect(hint?.hint).toContain('wait_for');
    });

    it('should hint to record skill memory after element_pick followed by successful same-node interact', () => {
      const tracker = makeTracker([
        { toolName: 'element_pick', args: { tabId: 'tab-1', action: 'start' }, result: 'success' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked picked element successfully');
      const hint = engine.getHint(
        'interact',
        result,
        false,
        'test',
        { target: { nodeRef: 'bnr:loader-1:42' } },
      );
      expect(hint?.rule).toBe('element-pick-skill-record');
      expect(hint?.hint).toContain('oc_skill_record');
      expect(hint?.hint).toContain('bnr:loader-1:42');
    });

    it('should hint after form submission', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Form submitted successfully');
      const hint = engine.getHint('fill_form', result, false);
      expect(hint?.hint).toContain('wait_for');
    });
  });

  describe('priority ordering', () => {
    it('error recovery should win over success hints', () => {
      // An error result that could match both error-recovery and success
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('timeout waiting for navigation', true);
      const hint = engine.getHint('navigate', result, true);
      // Should be error-recovery hint (lower priority number = higher precedence)
      expect(hint?.hint).toContain('wait_for');
    });
  });

  describe('repetition detection rules', () => {
    it('should detect same tool failing 3 times in a row', () => {
      const tracker = makeTracker([
        { toolName: 'interact', result: 'error', error: 'not found' },
        { toolName: 'interact', result: 'error', error: 'not found' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('element not found', true);
      const hint = engine.getHint('interact', result, true);
      // Error recovery (priority 100+) should fire before repetition (250) for known patterns
      // But for unknown error patterns, repetition catches it
      expect(hint).not.toBeNull();
    });

    it('should detect same-tool error streak for unknown errors (progress-tracker fires first)', () => {
      const tracker = makeTracker([
        { toolName: 'custom_tool', result: 'error', error: 'weird error' },
        { toolName: 'custom_tool', result: 'error', error: 'weird error' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('another weird error', true);
      const hint = engine.getHint('custom_tool', result, true);
      // ProgressTracker (priority 50) now fires before repetition detection (priority 250)
      // on 3+ consecutive errors, catching the "stuck" condition
      expect(hint).not.toBeNull();
      expect(hint?.rule).toBe('progress-tracker-stuck');
      expect(hint?.hint).toContain('different approach');
    });

    it('should detect A↔B oscillation pattern', () => {
      const tracker = makeTracker([
        { toolName: 'read_page' },
        { toolName: 'navigate' },
        { toolName: 'read_page' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('navigated to page');
      const hint = engine.getHint('navigate', result, false);
      expect(hint?.hint).toContain('oscillation');
      expect(hint?.hint).toContain('navigate');
      expect(hint?.hint).toContain('read_page');
    });

    it('should detect same tool called 3+ times with success', () => {
      const tracker = makeTracker([
        { toolName: 'read_page' },
        { toolName: 'read_page' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('page content unchanged');
      const hint = engine.getHint('read_page', result, false);
      // Sequence detection (repeated read_page, priority 301) fires before repetition (252)
      expect(hint).not.toBeNull();
    });

    it('should NOT trigger same-tool-same-result for batch-exempt tools (tabs_create, navigate, tabs_close)', () => {
      for (const tool of ['tabs_create', 'navigate', 'tabs_close']) {
        const tracker = makeTracker([
          { toolName: tool },
          { toolName: tool },
          { toolName: tool },
          { toolName: tool },
        ]);
        const engine = new HintEngine(tracker);
        const result = makeResult(`{"tabId":"abc","url":"https://example.com"}`);
        const hint = engine.getHint(tool, result, false);
        // Should not match same-tool-same-result; may match other rules (e.g. pagination for navigate)
        if (hint) {
          expect(hint.rule).not.toBe('same-tool-same-result');
        }
      }
    });

    it('should not trigger on mixed tool calls', () => {
      // makeTracker reverses input, so first element becomes most recent in getRecentCalls.
      // Put 'find' first so recentCalls[0]='find' — not an action trigger for state-check-after-action.
      const tracker = makeTracker([
        { toolName: 'find' },
        { toolName: 'navigate' },
        { toolName: 'interact' },
      ]);
      const engine = new HintEngine(tracker);
      // Warm up to consume setup-permission-hint
      engine.getHint('find', makeResult('warmup'), false);
      const result = makeResult('{"status":"ok"}');
      const hint = engine.getHint('read_page', result, false);
      expect(hint).toBeNull();
    });
  });

  describe('anti-wandering rules', () => {
    it('coordinate-click-stall: triggers on 3+ recent computer clicks', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (100, 200)');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('CLICK STALL');
    });

    it('coordinate-click-stall: does NOT trigger on only 2 recent clicks', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (100, 200)');
      const hint = engine.getHint('computer', result, false);
      // Another rule (same-tool-same-result) may match, but CLICK STALL should not
      if (hint) {
        expect(hint?.hint).not.toContain('CLICK STALL');
      }
    });

    it('screenshot-verification-loop: triggers on click+screenshot pattern', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'screenshot' } },
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'screenshot' } },
        { toolName: 'computer', args: { action: 'left_click' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (200, 300)');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('Multiple screenshots after clicks');
    });

    it('empty-result-streak: triggers after 3+ empty javascript_tool results', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('null');
      const hint = engine.getHint('javascript_tool', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('empty/null results');
      expect(hint?.hint).toContain('read_page');
    });

    it('empty-result-streak: treats undefined javascript_tool output as empty', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const hint = engine.getHint('javascript_tool', makeResult('undefined'), false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('empty/null results');
    });

    it('empty-result-streak: treats empty string javascript_tool output as empty', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const hint = engine.getHint('javascript_tool', makeResult(''), false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('empty/null results');
    });

    it('empty-result-streak: does NOT trigger when current result is non-empty', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('<div class="save-indicator">Saved</div>');
      const hint = engine.getHint('javascript_tool', result, false);
      if (hint) {
        expect(hint?.hint).not.toContain('empty/null results');
      }
    });

    it('empty-result-streak: wins over js-escalation-ladder (lower priority)', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('[]');
      const hint = engine.getHint('javascript_tool', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('empty/null results');
      expect(hint?.hint).not.toContain('escalation ladder');
    });

    it('js-escalation-ladder: triggers on 3+ javascript_tool calls with non-empty result', () => {
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('document.querySelector returned element data');
      const hint = engine.getHint('javascript_tool', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('escalation ladder');
    });

    it('post-scroll-click: triggers when scroll action precedes coordinate click', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'scroll' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (300, 400)');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('scroll');
    });

    it('contenteditable-click-hint: triggers when click hits a rich text editor', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Clicked at (50, 50) — Hit: div[contenteditable="true"]');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('rich text editor');
    });

    it('coordinate-click-after-read: triggers when clicking a non-interactive element', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Clicked at (200, 300) — Hit: span [not interactive]');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('non-interactive');
    });
  });

  describe('timeout and slow-page hints', () => {
    it('screenshot timeout gets specific hint', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Page.captureScreenshot timed out', true);
      const hint = engine.getHint('computer', result, true);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('read_page mode="dom"');
      expect(hint?.hint).toContain('Screenshot timed out');
    });

    it('generic timeout gets updated hint', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('Navigation timeout', true);
      const hint = engine.getHint('navigate', result, true);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('wait_for');
      expect(hint?.hint).not.toContain('may require login');
    });

    it('slow-page-warning fires after slow navigate', () => {
      const tracker = new ActivityTracker();
      // Seed a completed navigate call with duration > 5000ms using mocked Date.now
      // startCall calls Date.now() twice (callId + startTime), endCall once
      const startMs = 1000000;
      const endMs = startMs + 8000;
      let callCount = 0;
      const dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => {
        callCount++;
        return callCount <= 2 ? startMs : endMs;
      });
      const callId = tracker.startCall('navigate', 'test', { url: 'http://example.com' });
      tracker.endCall(callId, 'success');
      dateSpy.mockRestore();

      const engine = new HintEngine(tracker);
      // Current tool is computer (screenshot)
      const result = makeResult('screenshot captured');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint?.hint).toContain('Slow page detected');
    });

    it('slow-page-warning does not fire for fast pages', () => {
      const tracker = new ActivityTracker();
      // Seed a completed navigate call with duration 1000ms (fast)
      // startCall calls Date.now() twice (callId + startTime), endCall once
      const startMs = 1000000;
      const endMs = startMs + 1000;
      let callCount = 0;
      const dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => {
        callCount++;
        return callCount <= 2 ? startMs : endMs;
      });
      const callId = tracker.startCall('navigate', 'test', { url: 'http://example.com' });
      tracker.endCall(callId, 'success');
      dateSpy.mockRestore();

      const engine = new HintEngine(tracker);
      const result = makeResult('screenshot captured');
      // Use a result that won't match other rules
      const hint = engine.getHint('computer', result, false);
      // Should NOT contain slow-page-warning
      if (hint) {
        expect(hint?.hint).not.toContain('Slow page detected');
      }
    });
  });

  describe('hit/miss logging', () => {
    let tmpDir: string;

    beforeEach(() => {
      tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hint-log-'));
    });

    afterEach(() => {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    });

    it('should log hint hits to JSONL file', async () => {
      const engine = new HintEngine(new ActivityTracker());
      engine.enableLogging(tmpDir);

      const result = makeResult('ref not found: abc', true);
      engine.getHint('interact', result, true);

      // Flush buffered writes before reading
      engine.destroy();
      await new Promise(resolve => setTimeout(resolve, 50));

      const files = fs.readdirSync(tmpDir).filter(f => f.endsWith('.jsonl'));
      expect(files).toHaveLength(1);

      const lines = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8').trim().split('\n');
      expect(lines).toHaveLength(1);

      const entry = JSON.parse(lines[0]);
      expect(entry.toolName).toBe('interact');
      expect(entry.isError).toBe(true);
      expect(entry.matchedRule).toContain('error-recovery');
      expect(entry.hint).toContain('Refs expire');
      expect(entry.severity).toBe('info');
      expect(entry.fireCount).toBe(1);
    });

    it('should log hint misses with null values', async () => {
      const engine = new HintEngine(new ActivityTracker());
      engine.enableLogging(tmpDir);

      const result = makeResult('{"status":"ok"}');
      engine.getHint('some_tool', result, false);

      // Flush buffered writes before reading
      engine.destroy();
      await new Promise(resolve => setTimeout(resolve, 50));

      const files = fs.readdirSync(tmpDir).filter(f => f.endsWith('.jsonl'));
      const lines = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8').trim().split('\n');
      const entry = JSON.parse(lines[0]);
      expect(entry.matchedRule).toBeNull();
      expect(entry.hint).toBeNull();
      expect(entry.severity).toBeNull();
      expect(entry.fireCount).toBe(0);
    });

    it('should accumulate multiple log entries', async () => {
      const engine = new HintEngine(new ActivityTracker());
      engine.enableLogging(tmpDir);

      engine.getHint('navigate', makeResult('{"action":"navigate","url":"https://app.com","authRedirect":true}'), false);
      engine.getHint('find', makeResult('0 results'), false);
      engine.getHint('some_tool', makeResult('ok'), false);

      // Flush buffered writes before reading
      engine.destroy();
      await new Promise(resolve => setTimeout(resolve, 50));

      const files = fs.readdirSync(tmpDir).filter(f => f.endsWith('.jsonl'));
      const lines = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8').trim().split('\n');
      expect(lines).toHaveLength(3);
    });
  });

  describe('hint escalation', () => {
    it('should return info severity for first 2 firings', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);

      const hint1 = engine.getHint('interact', result, true);
      expect(hint1).not.toBeNull();
      expect(hint1!.severity).toBe('info');
      expect(hint1!.fireCount).toBe(1);

      const hint2 = engine.getHint('interact', result, true);
      expect(hint2!.severity).toBe('info');
      expect(hint2!.fireCount).toBe(2);
    });

    it('should escalate to warning severity at 3-4 firings', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);

      engine.getHint('interact', result, true);
      engine.getHint('interact', result, true);
      const hint3 = engine.getHint('interact', result, true);
      expect(hint3!.severity).toBe('warning');
      expect(hint3!.fireCount).toBe(3);
      expect(hint3!.hint).toContain('WARNING');

      const hint4 = engine.getHint('interact', result, true);
      expect(hint4!.severity).toBe('warning');
      expect(hint4!.fireCount).toBe(4);
    });

    it('should escalate to critical severity at 5+ firings', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);

      for (let i = 0; i < 4; i++) {
        engine.getHint('interact', result, true);
      }
      const hint5 = engine.getHint('interact', result, true);
      expect(hint5!.severity).toBe('critical');
      expect(hint5!.fireCount).toBe(5);
      expect(hint5!.hint).toContain('CRITICAL');
      expect(hint5!.hint).toContain('you MUST change approach');
    });

    it('should track fire counts independently per rule', () => {
      const engine = new HintEngine(new ActivityTracker());

      const errResult = makeResult('ref not found: abc', true);
      engine.getHint('interact', errResult, true);
      const hint2 = engine.getHint('interact', errResult, true);
      expect(hint2!.fireCount).toBe(2);

      const navResult = makeResult('{"action":"navigate","url":"https://app.com/dashboard","title":"App","authRedirect":true}');
      const loginHint = engine.getHint('navigate', navResult, false);
      expect(loginHint!.fireCount).toBe(1);

      const hint3 = engine.getHint('interact', errResult, true);
      expect(hint3!.fireCount).toBe(3);
      expect(hint3!.severity).toBe('warning');
    });
  });

  describe('maxSeverity cap', () => {
    it('should cap severity at maxSeverity when rule specifies it', () => {
      // Verify getSeverity respects maxSeverity via a rule that will match repeatedly.
      // error-recovery rule (no maxSeverity) should reach critical at 5+ firings.
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);

      for (let i = 0; i < 4; i++) {
        engine.getHint('interact', result, true);
      }
      const hint5 = engine.getHint('interact', result, true);
      expect(hint5!.severity).toBe('critical');
      expect(hint5!.fireCount).toBe(5);

      // Now verify the HintRule interface accepts maxSeverity (compile-time check)
      const rules = engine.getRules();
      const errorRule = rules.find(r => r.name === 'error-recovery-0');
      expect(errorRule).toBeDefined();
      expect(errorRule!.maxSeverity).toBeUndefined(); // no cap → critical allowed
    });

    it('should not escalate beyond maxSeverity cap', () => {
      const engine = new HintEngine(new ActivityTracker());
      const rules = engine.getRules();

      for (const rule of rules) {
        expect(rule.maxSeverity === undefined || ['info', 'warning', 'critical'].includes(rule.maxSeverity)).toBe(true);
      }
    });

    it('should cap repetition-detection advisory rules at warning', () => {
      // js-escalation-ladder has maxSeverity: 'warning'
      const tracker = makeTracker([
        { toolName: 'javascript_tool' },
        { toolName: 'javascript_tool' },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('data extracted');

      // Fire 10 times to exceed critical threshold
      let lastHint;
      for (let i = 0; i < 10; i++) {
        lastHint = engine.getHint('javascript_tool', result, false);
      }

      expect(lastHint).not.toBeNull();
      expect(lastHint!.rule).toBe('js-escalation-ladder');
      expect(lastHint!.fireCount).toBeGreaterThanOrEqual(5);
      expect(lastHint!.severity).toBe('warning');
      expect(lastHint!.hint).not.toContain('CRITICAL');
    });

    it('should cap navigate-to-demo at info severity', () => {
      const tracker = makeTracker([]);
      const engine = new HintEngine(tracker);
      const result = makeResult('{"url":"http://localhost:3000/page","title":"Dev App"}');

      let lastHint;
      for (let i = 0; i < 10; i++) {
        lastHint = engine.getHint('navigate', result, false);
      }

      if (lastHint && lastHint.rule === 'navigate-to-demo') {
        expect(lastHint.severity).toBe('info');
        expect(lastHint.hint).not.toContain('WARNING');
        expect(lastHint.hint).not.toContain('CRITICAL');
      }
    });

    it('should not have duplicate repeated-read-page rules', () => {
      const engine = new HintEngine(new ActivityTracker());
      const rules = engine.getRules();
      const readPageRules = rules.filter(r => r.name === 'repeated-read-page');
      expect(readPageRules).toHaveLength(1);
      expect(readPageRules[0].priority).toBe(207);
    });
  });

  describe('Progress Tracking Integration', () => {


    it('adds ranked recovery candidates to stuck stale-ref hints', () => {
      const tracker = makeTracker([
        { toolName: 'interact', result: 'error' },
        { toolName: 'interact', result: 'error' },
      ]);
      const engine = new HintEngine(tracker as any);
      const hint = engine.getHint('interact', makeResult('STALE_REF: element not found', true), true, undefined, { tabId: 'tab-1', ref: 'ref_1' });

      expect(hint!.rule).toBe('progress-tracker-stuck');
      expect(hint!.recoveryCandidates?.[0]).toMatchObject({ tool: 'read_page', risk: 'read_only' });
      expect(hint!.hint).toContain('Ranked recovery candidates');
      expect(hint!.hint).not.toContain('ref_1');
    });

    it('does not suggest blind interaction candidates for blocking pages', () => {
      const tracker = makeTracker([
        { toolName: 'click', result: 'error' },
        { toolName: 'click', result: 'error' },
      ]);
      const engine = new HintEngine(tracker as any);
      const hint = engine.getHint('click', makeResult('CAPTCHA Access Denied', true), true);

      expect(hint!.rule).toBe('progress-tracker-stuck');
      expect(hint!.recoveryCandidates?.map(c => c.tool)).toEqual(['read_page', 'tabs_context']);
      expect(hint!.recoveryCandidates?.every(c => c.risk === 'read_only')).toBe(true);
    });

    it('returns stuck hint when 3+ consecutive errors', () => {
      const tracker = makeTracker([
        { toolName: 'navigate', result: 'error', error: 'timed out' },
        { toolName: 'navigate', result: 'error', error: 'timed out' },
      ]);
      const engine = new HintEngine(tracker);
      const hint = engine.getHint('navigate', makeResult('timed out', true), true);
      expect(hint).not.toBeNull();
      expect(hint!.rule).toBe('progress-tracker-stuck');
      expect(hint!.severity).toBe('warning'); // first fire = warning
    });

    it('escalates stuck severity to critical on 2nd fire', () => {
      const tracker = makeTracker([
        { toolName: 'navigate', result: 'error', error: 'timed out' },
        { toolName: 'navigate', result: 'error', error: 'timed out' },
      ]);
      const engine = new HintEngine(tracker);

      // First fire → warning
      const hint1 = engine.getHint('navigate', makeResult('timed out', true), true);
      expect(hint1!.severity).toBe('warning');

      // Second fire → critical
      const hint2 = engine.getHint('navigate', makeResult('timed out', true), true);
      expect(hint2!.severity).toBe('critical');
    });

    it('returns stalling hint on 3 non-progress calls', () => {
      const tracker = makeTracker([
        { toolName: 'navigate', result: 'success', error: 'Login page detected' },
        { toolName: 'navigate', result: 'success', error: 'Login page detected' },
      ]);
      const engine = new HintEngine(tracker);
      const hint = engine.getHint('navigate', makeResult('Login page detected'), false);
      expect(hint).not.toBeNull();
      expect(hint!.rule).toBe('progress-tracker-stalling');
    });

    it('returns null (falls through to rules) when progressing', () => {
      const tracker = makeTracker([
        { toolName: 'navigate', result: 'success' },
        { toolName: 'read_page', result: 'success' },
      ]);
      const engine = new HintEngine(tracker);
      // Clean current result, no errors → progressing → no progress hint
      const hint = engine.getHint('read_page', makeResult('Page content here'), false);
      // Should either be null or a non-progress-tracker rule
      if (hint) {
        expect(hint.rule).not.toMatch(/^progress-tracker/);
      }
    });

    it('progress tracker takes priority over other rules', () => {
      // With 3 errors, progress-tracker-stuck should fire even though
      // error-recovery rules would also match
      const tracker = makeTracker([
        { toolName: 'computer', result: 'error', error: 'not found' },
        { toolName: 'computer', result: 'error', error: 'not found' },
      ]);
      const engine = new HintEngine(tracker);
      const hint = engine.getHint('computer', makeResult('not found', true), true);
      expect(hint).not.toBeNull();
      expect(hint!.rule).toBe('progress-tracker-stuck');
    });
  });

  describe('structured hint result', () => {
    it('should include rule name and severity', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint).not.toBeNull();
      expect(hint!.rule).toMatch(/^error-recovery/);
      expect(hint!.severity).toBe('info');
      expect(hint!.rawHint).toContain('Refs expire');
    });

    it('should extract context with coordinates', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (940, 43) \u2014 Hit: div.search-box');
      const hint = engine.getHint('computer', result, false);
      expect(hint).not.toBeNull();
      expect(hint!.context).toBeDefined();
      expect(hint!.context!.coordinates).toBe('(940, 43)');
      expect(hint!.context!.element).toBe('div.search-box');
    });

    it('should extract tool suggestion from hint text', () => {
      const engine = new HintEngine(new ActivityTracker());
      const result = makeResult('ref not found: abc', true);
      const hint = engine.getHint('interact', result, true);
      expect(hint!.suggestion).toBeDefined();
      expect(hint!.suggestion!.tool).toBe('read_page');
    });

    it('should include action history in critical hints', () => {
      const tracker = makeTracker([
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
        { toolName: 'computer', args: { action: 'left_click' } },
      ]);
      const engine = new HintEngine(tracker);
      const result = makeResult('Clicked at (100, 200)');

      for (let i = 0; i < 4; i++) {
        engine.getHint('computer', result, false);
      }
      const critical = engine.getHint('computer', result, false);
      expect(critical!.severity).toBe('critical');
      expect(critical!.hint).toContain('Previous actions:');
      expect(critical!.hint).toContain('computer');
    });
  });
});

describe('failure episode memory', () => {
  it('records a verified recovery episode and emits a future advisory hint', () => {
    const tracker = new ActivityTracker();
    const engine = new HintEngine(tracker);

    const first = engine.getHint(
      'custom_action',
      makeResult('opaque widget failure on https://example.test/form', true),
      true,
      's-episode',
      { url: 'https://example.test/form', description: 'submit contact form' },
    );
    expect(first).toBeNull();

    engine.getHint(
      'read_page',
      makeResult('success banner visible after dismiss overlay'),
      false,
      's-episode',
      { url: 'https://example.test/form', description: 'inspect page and dismiss overlay' },
    );

    const hint = engine.getHint(
      'custom_action',
      makeResult('opaque widget failure on https://example.test/form', true),
      true,
      's-episode',
      { url: 'https://example.test/form', description: 'submit contact form' },
    );

    expect(hint?.rule).toBe('learned-pattern');
    expect(hint?.hint).toContain('Similar failure episode');
    expect(hint?.hint).toContain('no recovery was auto-executed');
    expect(engine.getLearner().getFailureEpisodes()).toHaveLength(1);
  });
});
