/**
 * Tests for Graph Tools - link analysis and traversal
 *
 * These tests cover backlinks, forward links, orphan detection,
 * hub detection, and graph traversal operations.
 */

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

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

describe('Graph Tools via MCP', () => {
  let context: TestServerContext;
  let client: TestClient;

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

  // Retired tools (data available via search results):
  // get_backlinks, get_forward_links — kept for Crank plugin dependency
  // get_weighted_links — retired (search returns edge-weight-ranked links)

  describe('graph action=analyse: orphans', () => {
    test('finds notes with no backlinks', async () => {
      const result = await client.callTool('graph', { action: 'analyse', analysis: 'orphans' });

      const data = JSON.parse(result.content[0].text);
      expect(data.orphans).toBeDefined();
      expect(data.orphan_count).toBeGreaterThanOrEqual(0);
    });

    test('filters by folder', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'orphans',
        folder: 'edge-cases',
      });

      const data = JSON.parse(result.content[0].text);
      for (const orphan of data.orphans) {
        expect(orphan.path.startsWith('edge-cases/')).toBe(true);
      }
    });

    test('respects limit and offset', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'orphans',
        limit: 2,
        offset: 0,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.orphans.length).toBeLessThanOrEqual(2);
    });
  });

  describe('graph action=analyse: hubs', () => {
    test('finds highly connected notes', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'hubs',
        min_links: 1,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.hubs).toBeDefined();
      for (const hub of data.hubs) {
        expect(hub.total_connections).toBeGreaterThanOrEqual(1);
      }
    });

    test('respects min_links threshold', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'hubs',
        min_links: 5,
      });

      const data = JSON.parse(result.content[0].text);
      for (const hub of data.hubs) {
        expect(hub.total_connections).toBeGreaterThanOrEqual(5);
      }
    });

    test('respects limit and offset', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'hubs',
        limit: 1,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.hubs.length).toBeLessThanOrEqual(1);
    });
  });
});

describe('Advanced Graph Tools via MCP', () => {
  let context: TestServerContext;
  let client: TestClient;

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

  describe('graph action: path', () => {
    test('finds path between connected notes', async () => {
      const result = await client.callTool('graph', {
        action: 'path',
        from: 'normal-note.md',
        to: 'another-note.md',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.exists).toBeDefined();
      if (data.exists) {
        expect(data.path.length).toBeGreaterThan(0);
      }
    });

    test('returns empty path for unconnected notes', async () => {
      const result = await client.callTool('graph', {
        action: 'path',
        from: 'orphan-note.md',
        to: 'another-note.md',
      });

      const data = JSON.parse(result.content[0].text);
      // Orphan has no links, so no path to another note
      expect(data).toBeDefined();
    });

    test('handles same note as start and end', async () => {
      const result = await client.callTool('graph', {
        action: 'path',
        from: 'normal-note.md',
        to: 'normal-note.md',
      });

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

    test('respects max_depth parameter', async () => {
      const result = await client.callTool('graph', {
        action: 'path',
        from: 'normal-note.md',
        to: 'orphan-note.md',
        max_depth: 1,
      });

      const data = JSON.parse(result.content[0].text);
      if (data.exists) {
        expect(data.path.length - 1).toBeLessThanOrEqual(1);
      }
    });
  });

  describe('graph action: neighbors', () => {
    test('finds common targets between notes', async () => {
      const result = await client.callTool('graph', {
        action: 'neighbors',
        path_a: 'normal-note.md',
        path_b: 'acme-corp.md',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.common_neighbors).toBeDefined();
      expect(data.common_count).toBeDefined();
    });

    test('returns empty for notes with no common links', async () => {
      const result = await client.callTool('graph', {
        action: 'neighbors',
        path_a: 'orphan-note.md',
        path_b: 'another-note.md',
      });

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

    test('handles non-existent notes', async () => {
      const result = await client.callTool('graph', {
        action: 'neighbors',
        path_a: 'does-not-exist.md',
        path_b: 'normal-note.md',
      });

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

  describe('graph action: analyse', () => {
    test('supports mode-specific orphan analysis on merged graph tool', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'orphans',
        limit: 5,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.analysis).toBe('orphans');
      expect(Array.isArray(data.orphans)).toBe(true);
    });

    test('supports mode-specific dead-end analysis on merged graph tool', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'dead_ends',
        min_backlinks: 1,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.analysis).toBe('dead_ends');
      expect(Array.isArray(data.dead_ends)).toBe(true);
    });

    test('supports mode-specific immature-note analysis on merged graph tool', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'immature',
        limit: 5,
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.analysis).toBe('immature');
      expect(Array.isArray(data.notes)).toBe(true);
    });
  });

  describe('graph action=analyse: dead_ends', () => {
    test('finds notes with backlinks but no outlinks', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'dead_ends',
        min_backlinks: 1,
      });

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

    test('respects min_backlinks threshold', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'dead_ends',
        min_backlinks: 2,
      });

      const data = JSON.parse(result.content[0].text);
      for (const note of data.dead_ends) {
        expect(note.backlink_count).toBeGreaterThanOrEqual(2);
      }
    });

    test('filters by folder', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'dead_ends',
        folder: 'edge-cases',
      });

      const data = JSON.parse(result.content[0].text);
      for (const note of data.dead_ends) {
        expect(note.path.startsWith('edge-cases/')).toBe(true);
      }
    });
  });

  describe('graph action=analyse: sources', () => {
    test('finds notes with outlinks but no backlinks', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'sources',
        min_outlinks: 1,
      });

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

    test('respects min_outlinks threshold', async () => {
      const result = await client.callTool('graph', {
        action: 'analyse',
        analysis: 'sources',
        min_outlinks: 2,
      });

      const data = JSON.parse(result.content[0].text);
      for (const note of data.sources) {
        expect(note.outlink_count).toBeGreaterThanOrEqual(2);
      }
    });
  });

  describe('graph action: strength', () => {
    test('calculates connection strength between notes', async () => {
      const result = await client.callTool('graph', {
        action: 'strength',
        path_a: 'normal-note.md',
        path_b: 'another-note.md',
      });

      const data = JSON.parse(result.content[0].text);
      expect(data.score).toBeDefined();
      expect(typeof data.score).toBe('number');
      expect(data.factors).toBeDefined();
    });

    test('returns zero for unconnected notes', async () => {
      const result = await client.callTool('graph', {
        action: 'strength',
        path_a: 'orphan-note.md',
        path_b: 'another-note.md',
      });

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

    test('handles non-existent notes', async () => {
      const result = await client.callTool('graph', {
        action: 'strength',
        path_a: 'does-not-exist.md',
        path_b: 'normal-note.md',
      });

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

describe('Vault Index Graph Structure', () => {
  test('backlinks map contains arrays', async () => {
    const index = await buildVaultIndex(FIXTURES_PATH);

    for (const [, backlinks] of index.backlinks) {
      expect(Array.isArray(backlinks)).toBe(true);
    }
  });

  test('entities map points to paths', async () => {
    const index = await buildVaultIndex(FIXTURES_PATH);

    for (const [entity, path] of index.entities) {
      expect(typeof entity).toBe('string');
      expect(typeof path).toBe('string');
    }
  });

  test('notes contain outlinks with line numbers', async () => {
    const index = await buildVaultIndex(FIXTURES_PATH);

    for (const [, note] of index.notes) {
      for (const link of note.outlinks) {
        expect(link.target).toBeDefined();
        expect(typeof link.line).toBe('number');
      }
    }
  });
});
