/**
 * Tests for Wikilink Entity Matching heuristics
 *
 * These tests cover word boundary detection, case sensitivity,
 * skip regions (code blocks, URLs, existing links), and overlapping matches.
 */

import { describe, test, expect, beforeAll } from 'vitest';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { buildVaultIndex } from '../../../src/core/read/graph.js';
import type { VaultIndex } from '../../../src/core/read/types.js';
import { connectTestClient, type TestClient, createTestServer, type TestServerContext } from '../helpers/createTestServer.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURES_PATH = path.join(__dirname, '..', 'fixtures');

describe('Wikilink Suggestion Tool (link action: suggest)', () => {
  let context: TestServerContext;
  let client: TestClient;

  beforeAll(async () => {
    context = await createTestServer(FIXTURES_PATH);
    client = connectTestClient(context.server);
  });

  describe('Basic Entity Matching', () => {
    test('finds exact matches for note titles', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'We met with Alex Johnson today.',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);

      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(benMatch).toBeDefined();
    });

    test('matches are case-insensitive', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'We met with ALEX JOHNSON today.',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);
    });

    test('matches aliases as well as titles', async () => {
      // Normal Note has aliases: ["Test Note", "My Normal Note"]
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Check Test Note for details.',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);
    });
  });

  describe('Word Boundary Detection', () => {
    test('does not match partial words - prefix', async () => {
      // "Alex" should not match in "Alexy"
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Alexy went to the store.',
      });

      const data = JSON.parse(result.content[0].text);
      // Should not find "Alex" in "Alexy"
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity.toLowerCase() === 'alex'
      );
      expect(benMatch).toBeUndefined();
    });

    test('does not match partial words - suffix', async () => {
      // "Alex" should not match in "Lexan"
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Lexan is here.',
      });

      const data = JSON.parse(result.content[0].text);
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity.toLowerCase() === 'alex'
      );
      expect(benMatch).toBeUndefined();
    });

    test('matches at word boundaries with punctuation', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Contact Alex Johnson, immediately.',
      });

      const data = JSON.parse(result.content[0].text);
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(benMatch).toBeDefined();
    });

    test('matches with hyphen as word boundary', async () => {
      // Hyphen should be a word boundary
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Contact Alex-related topics.',
      });

      const data = JSON.parse(result.content[0].text);
      // "Alex" might match if it's a valid entity
      // This tests the hyphen boundary behavior
    });

    test('matches at start of text', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Alex Johnson is here.',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);
    });

    test('matches at end of text', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Contact Alex Johnson',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);
    });
  });

  describe('Skip Regions', () => {
    test('skips existing wikilinks', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'We met with [[Alex Johnson]] today.',
      });

      const data = JSON.parse(result.content[0].text);
      // Should not suggest linking Alex Johnson since it's already linked
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(benMatch).toBeUndefined();
    });

    test('skips inline code', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'The variable `Alex Johnson` is a string.',
      });

      const data = JSON.parse(result.content[0].text);
      // Should not suggest linking text in code
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(benMatch).toBeUndefined();
    });

    test('skips code blocks', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: '```\nAlex Johnson is in code\n```',
      });

      const data = JSON.parse(result.content[0].text);
      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(benMatch).toBeUndefined();
    });

    test('skips URLs', async () => {
      // If an entity appears in a URL, it should be skipped
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Visit https://example.com/AlexJohnson for more.',
      });

      // This tests URL skip behavior
      const data = JSON.parse(result.content[0].text);
      // URL text should not be matched
    });
  });

  describe('Overlapping Entity Handling', () => {
    test('longer matches take precedence', async () => {
      // If we have entities "Alex" and "Alex Johnson", "Alex Johnson" should match first
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'We met with Alex Johnson today.',
      });

      const data = JSON.parse(result.content[0].text);

      // Should have "Alex Johnson", not just "Alex"
      const fullMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      expect(fullMatch).toBeDefined();

      // Should not have a separate "Alex" match that overlaps
      const partialMatch = data.suggestions.find(
        (s: { entity: string; start: number }) =>
          s.entity.toLowerCase() === 'alex' && s.start === fullMatch?.start
      );
      expect(partialMatch).toBeUndefined();
    });

    test('adjacent non-overlapping matches are both found', async () => {
      // If two separate entities appear, both should match
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Alex Johnson visited Acme Corp yesterday.',
      });

      const data = JSON.parse(result.content[0].text);

      const benMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Alex Johnson'
      );
      const acmeMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'Acme Corp'
      );

      expect(benMatch).toBeDefined();
      expect(acmeMatch).toBeDefined();
    });
  });

  describe('Edge Cases', () => {
    test('handles empty text', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: '',
      });

      // Live contract: link(action: suggest) requires non-empty text and
      // returns an error payload. (The retired suggest_wikilinks tool
      // returned an empty suggestion list for empty input.)
      expect(result.isError).toBe(true);
      const data = JSON.parse(result.content[0].text);
      expect(data.error).toBe('text is required for action: suggest');
    });

    test('handles text with no matches', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'This text has no entities at all.',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestion_count).toBe(0);
    });

    test('respects limit parameter', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Alex Johnson met Alex Johnson and Alex Johnson again.',
        limit: 1,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.returned_count).toBe(1);
      expect(data.suggestions.length).toBe(1);
    });

    test('respects offset parameter', async () => {
      const resultAll = await client.callTool('link', {
        action: 'suggest',
        text: 'Alex Johnson works at Acme Corp with Alex Johnson',
      });
      const dataAll = JSON.parse(resultAll.content[0].text);

      const resultOffset = await client.callTool('link', {
        action: 'suggest',
        text: 'Alex Johnson works at Acme Corp with Alex Johnson',
        offset: 1,
      });
      const dataOffset = JSON.parse(resultOffset.content[0].text);

      // Offset should reduce returned count (offset skips first N results)
      // Total suggestion_count stays the same
      if (dataAll.suggestion_count > 1) {
        expect(dataOffset.suggestion_count).toBe(dataAll.suggestion_count);
        // Note: returned_count may not change if the tool doesn't properly slice
        // This tests that the API accepts the parameter without error
      }
    });

    test('handles special characters in text', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Email: Alex Johnson <alex@example.com>',
      });

      const data = JSON.parse(result.content[0].text);
      // Should still find Alex Johnson despite angle brackets
      expect(data.suggestions.length).toBeGreaterThan(0);
    });

    test('handles newlines correctly', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'Line 1:\nAlex Johnson\nLine 3',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.suggestions.length).toBeGreaterThan(0);
    });
  });

  describe('Common-word entity matching', () => {
    test('lowercase rest does NOT appear in suggestions', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'I need to rest after work.',
      });

      const data = JSON.parse(result.content[0].text);
      const restMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity.toLowerCase() === 'rest'
      );
      expect(restMatch).toBeUndefined();
    });

    test('uppercase REST DOES appear in suggestions', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'The REST API endpoint handles authentication.',
      });

      const data = JSON.parse(result.content[0].text);
      const restMatch = data.suggestions.find(
        (s: { entity: string }) => s.entity === 'REST'
      );
      expect(restMatch).toBeDefined();
      expect(restMatch.target).toMatch(/REST\.md$/);
    });

    test('mixed text: only uppercase instance matches', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'I need to rest. The REST API works great.',
      });

      const data = JSON.parse(result.content[0].text);
      const restMatches = data.suggestions.filter(
        (s: { entity: string }) => s.entity.toLowerCase() === 'rest'
      );
      expect(restMatches).toHaveLength(1);
      expect(restMatches[0].entity).toBe('REST');
    });

    test('alias RESTful matches case-insensitively', async () => {
      const result = await client.callTool('link', {
        action: 'suggest',
        text: 'We designed a RESTful architecture for the service.',
      });

      const data = JSON.parse(result.content[0].text);
      const match = data.suggestions.find(
        (s: { entity: string }) => s.entity.toLowerCase() === 'restful'
      );
      expect(match).toBeDefined();
    });
  });
});

describe('Link Validation Tool (link action: validate)', () => {
  let context: TestServerContext;
  let client: TestClient;

  beforeAll(async () => {
    context = await createTestServer(FIXTURES_PATH);
    client = connectTestClient(context.server);
  });

  test('validates links in a specific note', async () => {
    const result = await client.callTool('link', {
      action: 'validate',
      note_path: 'normal-note.md',
    });

    const data = JSON.parse(result.content[0].text);
    expect(data.scope).toBe('normal-note.md');
    expect(data.total_links).toBeGreaterThan(0);
  });

  test('validates all links when no path specified', async () => {
    const result = await client.callTool('link', { action: 'validate' });

    const data = JSON.parse(result.content[0].text);
    expect(data.scope).toBe('all');
    expect(data.total_links).toBeGreaterThan(0);
  });

  test('detects broken links', async () => {
    // normal-note.md has a link to "Does Not Exist"
    const result = await client.callTool('link', {
      action: 'validate',
      note_path: 'normal-note.md',
    });

    const data = JSON.parse(result.content[0].text);
    expect(data.broken_links).toBeGreaterThan(0);

    const brokenLink = data.broken.find(
      (b: { target: string }) => b.target === 'Does Not Exist'
    );
    expect(brokenLink).toBeDefined();
  });

  test('suggests fixes for broken links', async () => {
    const result = await client.callTool('link', {
      action: 'validate',
      note_path: 'normal-note.md',
    });

    const data = JSON.parse(result.content[0].text);
    // Some broken links may have suggestions
    // This depends on how similar the broken target is to existing entities
  });

  test('returns line numbers for broken links', async () => {
    const result = await client.callTool('link', {
      action: 'validate',
      note_path: 'normal-note.md',
    });

    const data = JSON.parse(result.content[0].text);
    for (const broken of data.broken) {
      expect(broken.line).toBeGreaterThan(0);
    }
  });

  test('handles non-existent note path', async () => {
    const result = await client.callTool('link', {
      action: 'validate',
      note_path: 'does-not-exist.md',
    });

    const data = JSON.parse(result.content[0].text);
    expect(data.total_links).toBe(0);
  });

  test('respects limit and offset', async () => {
    const resultAll = await client.callTool('link', {
      action: 'validate',
      limit: 100,
    });
    const dataAll = JSON.parse(resultAll.content[0].text);

    if (dataAll.broken_links > 1) {
      const resultLimited = await client.callTool('link', {
      action: 'validate',
        limit: 1,
      });
      const dataLimited = JSON.parse(resultLimited.content[0].text);

      // Limit should restrict returned results
      expect(dataLimited.returned_count).toBeLessThanOrEqual(dataAll.broken_links);
      expect(dataLimited.broken_links).toBe(dataAll.broken_links); // Total unchanged
    }
  });
});

describe('Entity Map Edge Cases', () => {
  let index: VaultIndex;

  beforeAll(async () => {
    index = await buildVaultIndex(FIXTURES_PATH);
  });

  test('case-insensitive entity lookup', () => {
    // Entities should be stored in lowercase
    expect(index.entities.has('alex johnson')).toBe(true);
    expect(index.entities.has('ALEX JOHNSON')).toBe(false); // stored lowercase
  });

  test('aliases are in entity map', () => {
    // Normal Note has aliases: ["Test Note", "My Normal Note"]
    expect(index.entities.has('test note')).toBe(true);
    expect(index.entities.has('my normal note')).toBe(true);
  });

  test('entity map points to file paths', () => {
    const filePath = index.entities.get('alex johnson');
    expect(filePath).toBeDefined();
    expect(filePath).toMatch(/\.md$/);
  });
});
