/**
 * E2E integration tests for the autocomplete workflow.
 *
 * Validates the complete flow from typing a trigger character through
 * skill selection and insertion, using a mock HTTP server that serves
 * both dashboard files and the /api/skills endpoint.
 *
 * Test scenarios from bd-3cc:
 * - Server serves /api/skills correctly
 * - Dashboard loads skills on session open
 * - Trigger character shows dropdown
 * - Keyboard navigation works
 * - Selection inserts skill
 * - Edge cases (wrong trigger, empty skills, backspace, etc.)
 */

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { getTriggerInfo, filteredSkills } from '../../dashboard/utils/autocomplete.js';

// -- Mock server for /api/skills --

const CLAUDE_SKILLS_RESPONSE = {
  trigger: '/',
  skills: [
    { name: 'commit', description: 'Create a git commit' },
    { name: 'comment', description: 'Add a comment' },
    { name: 'review-pr', description: 'Review a pull request' },
    { name: 'help', description: 'Get help' },
    { name: 'init', description: 'Initialize project' },
  ],
};

const CODEX_SKILLS_RESPONSE = {
  trigger: '$',
  skills: [
    { name: 'lint', description: 'Lint code' },
    { name: 'deploy', description: 'Deploy to prod' },
    { name: 'test', description: 'Run tests' },
  ],
};

const EMPTY_SKILLS_RESPONSE = {
  trigger: '/',
  skills: [],
};

let server;
let serverUrl;

function startMockServer() {
  return new Promise((resolve) => {
    server = createServer((req, res) => {
      const url = new URL(req.url, `http://${req.headers.host}`);

      if (url.pathname === '/api/skills') {
        const agent = url.searchParams.get('agent') || 'claude';
        res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });

        if (agent === 'codex') {
          res.end(JSON.stringify(CODEX_SKILLS_RESPONSE));
        } else if (agent === 'empty') {
          res.end(JSON.stringify(EMPTY_SKILLS_RESPONSE));
        } else {
          res.end(JSON.stringify(CLAUDE_SKILLS_RESPONSE));
        }
      } else {
        res.writeHead(404);
        res.end('Not found');
      }
    });

    server.listen(0, '127.0.0.1', () => {
      const { port } = server.address();
      serverUrl = `http://127.0.0.1:${port}`;
      resolve();
    });
  });
}

function stopMockServer() {
  return new Promise((resolve) => {
    if (server) server.close(resolve);
    else resolve();
  });
}

// -- Helper: simulate fetching skills like the dashboard does --

async function fetchSkills(agent) {
  const url = `${serverUrl}/api/skills?agent=${encodeURIComponent(agent)}`;
  const response = await fetch(url);
  if (!response.ok) return null;
  return response.json();
}

// =============================================================
// Test Suite: Server -> Client Skills Fetch
// =============================================================

describe('E2E: Server serves /api/skills correctly', () => {
  before(startMockServer);
  after(stopMockServer);

  it('fetches Claude skills with / trigger', async () => {
    const config = await fetchSkills('claude');
    assert.equal(config.trigger, '/');
    assert.ok(config.skills.length > 0, 'should have skills');
    assert.ok(config.skills.some(s => s.name === 'commit'));
  });

  it('fetches Codex skills with $ trigger', async () => {
    const config = await fetchSkills('codex');
    assert.equal(config.trigger, '$');
    assert.ok(config.skills.some(s => s.name === 'lint'));
  });

  it('returns empty skills list when none exist', async () => {
    const config = await fetchSkills('empty');
    assert.equal(config.trigger, '/');
    assert.deepEqual(config.skills, []);
  });

  it('each skill has name and description', async () => {
    const config = await fetchSkills('claude');
    for (const skill of config.skills) {
      assert.ok(skill.name, 'skill should have name');
      assert.ok(skill.description, 'skill should have description');
    }
  });
});

// =============================================================
// Test Suite: Dashboard loads skills on session open
// =============================================================

describe('E2E: Dashboard loads skills on session open', () => {
  before(startMockServer);
  after(stopMockServer);

  it('loads Claude skills config matching server response', async () => {
    const config = await fetchSkills('claude');
    assert.equal(config.trigger, '/');
    // Verify the config is usable by autocomplete functions
    const info = getTriggerInfo('/com', 4, config);
    assert.ok(info, 'should detect trigger in loaded config');
    assert.equal(info.filterText, 'com');
  });

  it('loads Codex skills config matching server response', async () => {
    const config = await fetchSkills('codex');
    assert.equal(config.trigger, '$');
    const info = getTriggerInfo('$li', 3, config);
    assert.ok(info, 'should detect $ trigger');
    assert.equal(info.filterText, 'li');
  });

  it('handles null/missing config gracefully', async () => {
    // Simulate network failure
    const info = getTriggerInfo('/test', 5, null);
    assert.equal(info, null);
    const skills = filteredSkills(null, { filterText: '' });
    assert.deepEqual(skills, []);
  });
});

// =============================================================
// Test Suite: Trigger character shows dropdown
// =============================================================

describe('E2E: Trigger character shows dropdown', () => {
  const config = CLAUDE_SKILLS_RESPONSE;

  it('Claude session: Type "/" -> dropdown appears with Claude skills', () => {
    const info = getTriggerInfo('/', 1, config);
    assert.ok(info, 'trigger should be detected');
    assert.equal(info.trigger, '/');
    const skills = filteredSkills(config, info);
    assert.ok(skills.length > 0, 'should show skills');
  });

  it('Codex session: Type "$" -> dropdown appears with Codex skills', () => {
    const codexConfig = CODEX_SKILLS_RESPONSE;
    const info = getTriggerInfo('$', 1, codexConfig);
    assert.ok(info, 'trigger should be detected');
    assert.equal(info.trigger, '$');
    const skills = filteredSkills(codexConfig, info);
    assert.ok(skills.length > 0);
  });

  it('Claude session: Type "$" -> nothing happens (wrong trigger)', () => {
    const info = getTriggerInfo('$', 1, config);
    assert.equal(info, null, 'wrong trigger should not activate');
  });

  it('Type "/com" -> list filters to skills containing "com"', () => {
    const info = getTriggerInfo('/com', 4, config);
    assert.ok(info);
    assert.equal(info.filterText, 'com');
    const skills = filteredSkills(config, info);
    const names = skills.map(s => s.name);
    assert.ok(names.includes('commit'), 'should include commit');
    assert.ok(names.includes('comment'), 'should include comment');
    assert.ok(!names.includes('review-pr'), 'should not include review-pr');
    assert.ok(!names.includes('help'), 'should not include help');
  });

  it('Mid-message: Type "please run /commit" -> autocomplete triggers on "/"', () => {
    const input = 'please run /commit';
    const info = getTriggerInfo(input, input.length, config);
    assert.ok(info, 'should detect trigger mid-message');
    assert.equal(info.trigger, '/');
    assert.equal(info.filterText, 'commit');
    assert.equal(info.replaceStart, 11);
    assert.equal(info.replaceEnd, 18);
  });

  it('Trigger at start of line after newline', () => {
    const input = 'first line\n/rev';
    const info = getTriggerInfo(input, input.length, config);
    assert.ok(info);
    assert.equal(info.filterText, 'rev');
  });
});

// =============================================================
// Test Suite: Keyboard navigation works
// =============================================================

describe('E2E: Keyboard navigation simulation', () => {
  const config = CLAUDE_SKILLS_RESPONSE;

  it('Arrow keys navigate through filtered list', () => {
    const info = getTriggerInfo('/', 1, config);
    const skills = filteredSkills(config, info);

    // Simulate state: selectedIndex starts at 0
    let selectedIndex = 0;

    // ArrowDown moves to next
    selectedIndex = Math.min(selectedIndex + 1, skills.length - 1);
    assert.equal(selectedIndex, 1);

    // ArrowDown again
    selectedIndex = Math.min(selectedIndex + 1, skills.length - 1);
    assert.equal(selectedIndex, 2);

    // ArrowUp moves back
    selectedIndex = Math.max(selectedIndex - 1, 0);
    assert.equal(selectedIndex, 1);

    // ArrowUp back to start
    selectedIndex = Math.max(selectedIndex - 1, 0);
    assert.equal(selectedIndex, 0);

    // ArrowUp at top doesn't go negative
    selectedIndex = Math.max(selectedIndex - 1, 0);
    assert.equal(selectedIndex, 0);
  });

  it('ArrowDown clamps at list end', () => {
    const info = getTriggerInfo('/com', 4, config);
    const skills = filteredSkills(config, info);
    // "com" matches commit and comment -> 2 skills
    assert.equal(skills.length, 2);

    let selectedIndex = 0;
    // Down to 1
    selectedIndex = Math.min(selectedIndex + 1, skills.length - 1);
    assert.equal(selectedIndex, 1);
    // Down again - clamped at 1
    selectedIndex = Math.min(selectedIndex + 1, skills.length - 1);
    assert.equal(selectedIndex, 1, 'should clamp at list end');
  });

  it('Enter selects the current skill', () => {
    const info = getTriggerInfo('/', 1, config);
    const skills = filteredSkills(config, info);
    const selectedIndex = 0;

    // Simulate Enter: select skill at selectedIndex
    const selected = skills[selectedIndex];
    assert.ok(selected, 'should have a skill to select');
    assert.equal(selected.name, skills[0].name);
  });

  it('Escape dismisses without selection', () => {
    // Simulate Escape: set showAutocomplete = false, no insertion
    let showAutocomplete = true;
    // Escape handler
    showAutocomplete = false;
    assert.equal(showAutocomplete, false, 'dropdown should close on Escape');
  });
});

// =============================================================
// Test Suite: Selection inserts skill
// =============================================================

describe('E2E: Selection inserts skill', () => {
  const config = CLAUDE_SKILLS_RESPONSE;

  /**
   * Simulate insertSkill logic from SimpleInput.js
   */
  function simulateInsertSkill(text, triggerInfo, skill, trigger) {
    const { replaceStart, replaceEnd } = triggerInfo;
    const before = text.slice(0, replaceStart);
    const after = text.slice(replaceEnd);
    const inserted = `${trigger}${skill.name} `;
    return {
      newText: before + inserted + after,
      newCursorPos: replaceStart + inserted.length,
    };
  }

  it('Selected skill shows as "{trigger}skill-name " in input', () => {
    const text = '/com';
    const info = getTriggerInfo(text, 4, config);
    const skills = filteredSkills(config, info);
    const skill = skills.find(s => s.name === 'commit');

    const { newText, newCursorPos } = simulateInsertSkill(text, info, skill, config.trigger);
    assert.equal(newText, '/commit ', 'should insert trigger + skill name + space');
    assert.equal(newCursorPos, 8, 'cursor should be after inserted text');
  });

  it('Inserting mid-message preserves surrounding text', () => {
    const text = 'please run /com and continue';
    const info = getTriggerInfo(text, 15, config); // cursor at end of "/com"
    assert.ok(info);
    const skill = { name: 'commit' };

    const { newText } = simulateInsertSkill(text, info, skill, config.trigger);
    assert.equal(newText, 'please run /commit  and continue');
    // Note: there's a double space because "and" was after the cursor position
    // In real use, the cursor was at position 15 which is after "/com"
  });

  it('Inserting at start of input', () => {
    const text = '/';
    const info = getTriggerInfo(text, 1, config);
    const skill = { name: 'help' };

    const { newText, newCursorPos } = simulateInsertSkill(text, info, skill, config.trigger);
    assert.equal(newText, '/help ');
    assert.equal(newCursorPos, 6);
  });

  it('Inserting with filter text replaces trigger+filter', () => {
    const text = '/review';
    const info = getTriggerInfo(text, 7, config);
    const skill = { name: 'review-pr' };

    const { newText } = simulateInsertSkill(text, info, skill, config.trigger);
    assert.equal(newText, '/review-pr ');
  });
});

// =============================================================
// Test Suite: Verify alphabetical ordering
// =============================================================

describe('E2E: Verify alphabetical ordering of skills', () => {
  it('Skills are returned sorted alphabetically', () => {
    const config = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('/', 1, config);
    const skills = filteredSkills(config, info);
    const names = skills.map(s => s.name);

    for (let i = 1; i < names.length; i++) {
      assert.ok(
        names[i].localeCompare(names[i - 1]) >= 0,
        `${names[i]} should come after ${names[i - 1]}`
      );
    }
  });

  it('Filtered results maintain alphabetical order', () => {
    const config = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('/com', 4, config);
    const skills = filteredSkills(config, info);
    const names = skills.map(s => s.name);

    assert.deepEqual(names, ['comment', 'commit']);
  });
});

// =============================================================
// Test Suite: Edge Cases
// =============================================================

describe('E2E: Edge cases', () => {
  it('Session without skills shows empty list', () => {
    const emptyConfig = EMPTY_SKILLS_RESPONSE;
    const info = getTriggerInfo('/', 1, emptyConfig);
    assert.ok(info, 'trigger still detected');
    const skills = filteredSkills(emptyConfig, info);
    assert.equal(skills.length, 0);
  });

  it('Single skill still shows in dropdown', () => {
    const singleConfig = {
      trigger: '/',
      skills: [{ name: 'only-skill', description: 'The only one' }],
    };
    const info = getTriggerInfo('/', 1, singleConfig);
    const skills = filteredSkills(singleConfig, info);
    assert.equal(skills.length, 1);
    assert.equal(skills[0].name, 'only-skill');
  });

  it('Multiple triggers in one message work independently', () => {
    const config = CLAUDE_SKILLS_RESPONSE;

    // User types: "first /commit then /review-pr finally"
    // After first insertion, simulating second trigger
    const text = 'first /commit then /rev';

    // Cursor at end - should detect second trigger
    const info = getTriggerInfo(text, text.length, config);
    assert.ok(info, 'should detect second trigger');
    assert.equal(info.filterText, 'rev');
    assert.equal(info.replaceStart, 19); // position of second "/"
    assert.equal(info.replaceEnd, text.length);

    const skills = filteredSkills(config, info);
    assert.ok(skills.some(s => s.name === 'review-pr'));
  });

  it('Backspace over trigger dismisses autocomplete', () => {
    const config = CLAUDE_SKILLS_RESPONSE;

    // Type "/" - trigger detected
    let info = getTriggerInfo('/', 1, config);
    assert.ok(info, 'trigger detected');

    // Backspace - text is now empty
    info = getTriggerInfo('', 0, config);
    assert.equal(info, null, 'trigger dismissed after backspace');
  });

  it('Trigger embedded in word does not activate', () => {
    const config = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('path/to/file', 5, config);
    assert.equal(info, null, 'should not trigger on path separator');
  });

  it('No matching skills after filtering shows empty list', () => {
    const config = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('/zzz', 4, config);
    assert.ok(info, 'trigger still detected');
    const skills = filteredSkills(config, info);
    assert.equal(skills.length, 0, 'no skills match "zzz"');
  });

  it('Case-insensitive filtering works', () => {
    const config = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('/COM', 4, config);
    assert.ok(info);
    assert.equal(info.filterText, 'com'); // lowercased
    const skills = filteredSkills(config, info);
    assert.ok(skills.length >= 2, 'should match commit and comment');
  });

  it('Click outside dismisses (state simulation)', () => {
    // Simulate: showAutocomplete=true, click outside sets it to false
    let showAutocomplete = true;
    // Simulate click outside handler
    const clickTarget = { contains: () => false };
    const textareaRef = { contains: () => false };
    if (!clickTarget.contains('event') && !textareaRef.contains('event')) {
      showAutocomplete = false;
    }
    assert.equal(showAutocomplete, false, 'click outside should dismiss');
  });
});

// =============================================================
// Test Suite: Cross-agent isolation
// =============================================================

describe('E2E: Cross-agent trigger isolation', () => {
  it('Claude trigger / does not activate in Codex config', () => {
    const codexConfig = CODEX_SKILLS_RESPONSE;
    const info = getTriggerInfo('/', 1, codexConfig);
    assert.equal(info, null, '/ should not trigger for Codex');
  });

  it('Codex trigger $ does not activate in Claude config', () => {
    const claudeConfig = CLAUDE_SKILLS_RESPONSE;
    const info = getTriggerInfo('$', 1, claudeConfig);
    assert.equal(info, null, '$ should not trigger for Claude');
  });

  it('Each agent gets its own skills list', async () => {
    // This requires the mock server
    await startMockServer();
    try {
      const claude = await fetchSkills('claude');
      const codex = await fetchSkills('codex');

      assert.equal(claude.trigger, '/');
      assert.equal(codex.trigger, '$');

      const claudeNames = claude.skills.map(s => s.name);
      const codexNames = codex.skills.map(s => s.name);

      // No overlap in default test data
      assert.ok(!claudeNames.includes('lint'), 'Claude should not have Codex skills');
      assert.ok(!codexNames.includes('commit'), 'Codex should not have Claude skills');
    } finally {
      await stopMockServer();
    }
  });
});

// =============================================================
// Test Suite: Full workflow simulation
// =============================================================

describe('E2E: Full autocomplete workflow', () => {
  before(startMockServer);
  after(stopMockServer);

  it('complete flow: fetch -> type -> filter -> navigate -> select -> verify', async () => {
    // Step 1: Fetch skills from server (like Modal.js does on session open)
    const config = await fetchSkills('claude');
    assert.equal(config.trigger, '/');
    assert.ok(config.skills.length > 0);

    // Step 2: User starts typing - no trigger yet
    let text = 'hello ';
    let cursorPos = text.length;
    let info = getTriggerInfo(text, cursorPos, config);
    assert.equal(info, null, 'no trigger yet');

    // Step 3: User types trigger character
    text = 'hello /';
    cursorPos = text.length;
    info = getTriggerInfo(text, cursorPos, config);
    assert.ok(info, 'trigger detected');
    let skills = filteredSkills(config, info);
    assert.ok(skills.length === 5, 'all 5 skills shown');

    // Step 4: User types filter text
    text = 'hello /com';
    cursorPos = text.length;
    info = getTriggerInfo(text, cursorPos, config);
    assert.ok(info);
    assert.equal(info.filterText, 'com');
    skills = filteredSkills(config, info);
    assert.equal(skills.length, 2, 'filtered to 2 skills');
    assert.deepEqual(skills.map(s => s.name), ['comment', 'commit']);

    // Step 5: Arrow down to select "commit" (index 1)
    let selectedIndex = 0; // starts on "comment"
    selectedIndex = Math.min(selectedIndex + 1, skills.length - 1); // ArrowDown
    assert.equal(selectedIndex, 1);
    assert.equal(skills[selectedIndex].name, 'commit');

    // Step 6: Press Enter to insert
    const selected = skills[selectedIndex];
    const { replaceStart, replaceEnd } = info;
    const before = text.slice(0, replaceStart);
    const after = text.slice(replaceEnd);
    const inserted = `${config.trigger}${selected.name} `;
    const newText = before + inserted + after;
    const newCursorPos = replaceStart + inserted.length;

    // Step 7: Verify insertion
    assert.equal(newText, 'hello /commit ');
    assert.equal(newCursorPos, 14);

    // Step 8: Verify autocomplete closed (trigger info should be null for the new text)
    // After insertion, cursor is at 14, no active trigger word
    const postInfo = getTriggerInfo(newText, newCursorPos, config);
    assert.equal(postInfo, null, 'autocomplete should be dismissed after selection');
  });

  it('complete flow with second trigger after first insertion', async () => {
    const config = await fetchSkills('claude');

    // After first insertion: "hello /commit "
    let text = 'hello /commit ';
    let cursorPos = text.length;

    // User types more text and another trigger
    text = 'hello /commit then /';
    cursorPos = text.length;
    let info = getTriggerInfo(text, cursorPos, config);
    assert.ok(info, 'second trigger detected');
    assert.equal(info.replaceStart, 19);

    // Filter the second trigger
    text = 'hello /commit then /rev';
    cursorPos = text.length;
    info = getTriggerInfo(text, cursorPos, config);
    assert.ok(info);
    assert.equal(info.filterText, 'rev');

    const skills = filteredSkills(config, info);
    assert.ok(skills.some(s => s.name === 'review-pr'));

    // Select review-pr
    const skill = skills.find(s => s.name === 'review-pr');
    const before = text.slice(0, info.replaceStart);
    const after = text.slice(info.replaceEnd);
    const newText = before + `${config.trigger}${skill.name} ` + after;

    assert.equal(newText, 'hello /commit then /review-pr ');
  });
});
