/**
 * MCP Write Integration Tests
 *
 * Tests write tools at the MCP client level — the same interface
 * Claude sees. Covers mutations, config, security boundaries, and undo.
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { initializeEntityIndex } from '../../../src/core/write/wikilinks.js';
import {
  createWriteTestServer,
  type WriteTestServerContext,
} from '../../helpers/createWriteTestServer.js';
import { createEntityCacheInStateDb, createTestNote } from '../helpers/testUtils.js';

/** Parse the first text content block from a tool result */
function parseResult(result: { content: unknown }): unknown {
  const content = result.content as Array<{ type: string; text: string }>;
  return JSON.parse(content[0].text);
}

describe('MCP Write Integration', () => {
  let ctx: WriteTestServerContext;
  let client: Client;

  beforeAll(async () => {
    ctx = await createWriteTestServer();

    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    await ctx.server.connect(serverTransport);

    client = new Client(
      { name: 'write-test', version: '1.0.0' },
      { capabilities: {} },
    );
    await client.connect(clientTransport);
  });

  describe('entity (action: alias) compatibility absorption', () => {
    it('absorbs a source name into a target note, rewrites backlinks, and deletes the source note', async () => {
      await createTestNote(ctx.vaultPath, 'people/Bar.md', [
        '# Bar',
        '',
        'Target note.',
      ].join('\n'));

      await createTestNote(ctx.vaultPath, 'people/Foo.md', [
        '# Foo',
        '',
        'Source note.',
      ].join('\n'));

      await createTestNote(ctx.vaultPath, 'notes/ref.md', [
        '# Ref',
        '',
        'See [[Foo]] for context.',
      ].join('\n'));

      const result = await client.callTool({
        name: 'entity',
        arguments: {
          action: 'alias',
          source_name: 'Foo',
          target_path: 'people/Bar.md',
        },
      });

      expect(result.isError).toBeFalsy();
      const data = parseResult(result) as Record<string, unknown>;
      expect(data.success).toBe(true);
      expect(data.backlinks_updated).toBe(1);

      const targetContent = readFileSync(path.join(ctx.vaultPath, 'people/Bar.md'), 'utf-8');
      expect(targetContent).toContain('aliases:');
      expect(targetContent).toContain('Foo');

      const refContent = readFileSync(path.join(ctx.vaultPath, 'notes/ref.md'), 'utf-8');
      expect(refContent).toContain('[[Bar|Foo]]');

      expect(existsSync(path.join(ctx.vaultPath, 'people/Foo.md'))).toBe(false);
    });
  });

  afterAll(async () => {
    await client?.close();
    await ctx?.cleanup();
  });

  // ==========================================================================
  // edit_section (action: 'add')
  // ==========================================================================
  describe('edit_section (action: add)', () => {
    it('adds content to an existing section', async () => {
      await createTestNote(ctx.vaultPath, 'test-add.md', [
        '# Test',
        '',
        '## Log',
        '',
        '- Existing entry',
        '',
      ].join('\n'));

      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'test-add.md',
          section: 'Log',
          content: '- New entry',
        },
      });

      expect(result.isError).toBeFalsy();
      const data = parseResult(result) as Record<string, unknown>;
      expect(data.success).toBe(true);

      const fileContent = readFileSync(path.join(ctx.vaultPath, 'test-add.md'), 'utf-8');
      expect(fileContent).toContain('- New entry');
      expect(fileContent).toContain('- Existing entry');
    });

    it('creates a note when create_if_missing is true', async () => {
      // create_if_missing creates note from fallback, then auto-creates missing section
      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'daily/2099-01-01.md',
          section: 'Log',
          content: '- Created via MCP',
          create_if_missing: true,
        },
      });

      expect(result.isError).toBeFalsy();

      const created = existsSync(path.join(ctx.vaultPath, 'daily/2099-01-01.md'));
      expect(created).toBe(true);

      const fileContent = readFileSync(path.join(ctx.vaultPath, 'daily/2099-01-01.md'), 'utf-8');
      expect(fileContent).toContain('Created via [[MCP]]');
      // Section was auto-created since it wasn't in the fallback template
      expect(fileContent).toContain('## Log');
    });

    it('routes sharded audit appends to bounded log shard notes', async () => {
      await createTestNote(ctx.vaultPath, 'daily-notes/2099-02-03.md', [
        '---',
        'type: daily',
        "date: '2099-02-03'",
        '---',
        '# 2099-02-03',
        '',
        '## Log',
        '',
      ].join('\n'));

      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'daily-notes/2099-02-03.md',
          section: 'Log',
          content: '**10:00** 🐵 @alice (1.2s)',
          children: [{ label: '**Response:**', content: 'Operational note for [[Project Alpha]]' }],
          format: 'plain',
          skipWikilinks: true,
          suggestOutgoingLinks: false,
          shard: {
            enabled: true,
            pattern: 'daily-notes/logs/{date}-audit-{index}.md',
            maxBytes: 262144,
            maxEntries: 250,
            mode: 'audit',
            lightIndex: true,
          },
        },
      });

      expect(result.isError).toBeFalsy();
      const data = parseResult(result) as Record<string, unknown>;
      expect(data.success).toBe(true);
      expect(data.path).toBe('daily-notes/2099-02-03.md');
      expect(data.shardPath).toBe('daily-notes/logs/2099-02-03-audit-001.md');
      expect(data.shardCreated).toBe(true);

      const canonical = readFileSync(path.join(ctx.vaultPath, 'daily-notes/2099-02-03.md'), 'utf-8');
      expect(canonical).toContain('[[daily-notes/logs/2099-02-03-audit-001|Audit log shard 001]]');
      expect(canonical).not.toContain('@alice');

      const shard = readFileSync(path.join(ctx.vaultPath, 'daily-notes/logs/2099-02-03-audit-001.md'), 'utf-8');
      expect(shard).toContain('type: daily-log-shard');
      expect(shard).toContain('flywheel_indexing: light');
      expect(shard).toContain("parent: '[[daily-notes/2099-02-03]]'");
      expect(shard).toContain('- **10:00** 🐵 @alice (1.2s)');
      expect(shard).toContain('Operational note for [[Project Alpha]]');
    });

    it('rolls sharded audit appends when the entry count limit is reached', async () => {
      await createTestNote(ctx.vaultPath, 'daily-notes/2099-02-04.md', [
        '---',
        'type: daily',
        "date: '2099-02-04'",
        '---',
        '# 2099-02-04',
        '',
        '## Log',
        '',
      ].join('\n'));

      await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'daily-notes/2099-02-04.md',
          section: 'Log',
          content: '**10:00** first',
          shard: {
            enabled: true,
            pattern: 'daily-notes/logs/{date}-audit-{index}.md',
            maxBytes: 262144,
            maxEntries: 1,
            mode: 'audit',
            lightIndex: true,
          },
        },
      });

      const second = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'daily-notes/2099-02-04.md',
          section: 'Log',
          content: '**10:01** second',
          shard: {
            enabled: true,
            pattern: 'daily-notes/logs/{date}-audit-{index}.md',
            maxBytes: 262144,
            maxEntries: 1,
            mode: 'audit',
            lightIndex: true,
          },
        },
      });

      const data = parseResult(second) as Record<string, unknown>;
      expect(data.shardPath).toBe('daily-notes/logs/2099-02-04-audit-002.md');
      expect(existsSync(path.join(ctx.vaultPath, 'daily-notes/logs/2099-02-04-audit-001.md'))).toBe(true);
      expect(existsSync(path.join(ctx.vaultPath, 'daily-notes/logs/2099-02-04-audit-002.md'))).toBe(true);
    });

    it('dry_run + create_if_missing does not create a file', async () => {
      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: 'daily/2099-02-02.md',
          section: '2099-02-02',
          content: '- Should not exist',
          create_if_missing: true,
          dry_run: true,
        },
      });

      expect(result.isError).toBeFalsy();
      const text = (result.content as Array<{ text: string }>)[0].text;
      expect(text.toLowerCase()).toContain('dry run');

      const created = existsSync(path.join(ctx.vaultPath, 'daily/2099-02-02.md'));
      expect(created).toBe(false);
    });

    it('blocks path traversal', async () => {
      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: '../../../etc/passwd',
          section: 'Log',
          content: 'exploit',
        },
      });

      const text = (result.content as Array<{ text: string }>)[0].text.toLowerCase();
      expect(text).toMatch(/path|blocked|invalid|outside|traversal|security/);
    });

    it('blocks sensitive file access', async () => {
      const result = await client.callTool({
        name: 'edit_section',
        arguments: {
          action: 'add',
          path: '.env',
          section: 'Secrets',
          content: 'exploit',
        },
      });

      const text = (result.content as Array<{ text: string }>)[0].text.toLowerCase();
      expect(text).toMatch(/sensitive|blocked|denied|protected|security|\.env/);
    });

    describe('edit_section children', () => {
      it('renders child labels as nested bullets', async () => {
        await createTestNote(ctx.vaultPath, 'child.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        const result = await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child.md',
            section: 'Log',
            content: 'Summary',
            suggestOutgoingLinks: false,
            children: [
              { label: '**Result:**', content: 'Passed' },
              { label: '**Owner:**', content: 'Jordan Smith' },
            ],
          },
        });
        expect(result.isError).toBeFalsy();

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child.md'), 'utf-8');
        expect(fileContent).toContain('- Summary');
        expect(fileContent).toContain('  - **Result:** Passed');
        expect(fileContent).toContain('  - **Owner:** [[Jordan Smith]]');
      });

      it('applies wikilinks inside child content', async () => {
        await createTestNote(ctx.vaultPath, 'people/Jordan Smith.md', [
          '# Jordan Smith',
          '',
          'Person note.',
        ].join('\n'));
        createEntityCacheInStateDb(ctx.stateDb, ctx.vaultPath, {
          people: ['Jordan Smith'],
        });
        await initializeEntityIndex(ctx.vaultPath);

        await createTestNote(ctx.vaultPath, 'child-links.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        const result = await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child-links.md',
            section: 'Log',
            content: 'Summary',
            suggestOutgoingLinks: false,
            children: [
              { label: '**Owner:**', content: 'Jordan Smith reviewed this.' },
            ],
          },
        });
        expect(result.isError).toBeFalsy();

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child-links.md'), 'utf-8');
        expect(fileContent).toContain('  - **Owner:** [[Jordan Smith]] reviewed this.');
      });

      it('indents multi-line child content', async () => {
        await createTestNote(ctx.vaultPath, 'child-indent.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        const result = await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child-indent.md',
            section: 'Log',
            content: 'Update',
            suggestOutgoingLinks: false,
            children: [
              { label: '**Notes:**', content: 'Line 1\nLine 2' },
            ],
          },
        });
        expect(result.isError).toBeFalsy();

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child-indent.md'), 'utf-8');
        // Child bullet line
        expect(fileContent).toContain('  - **Notes:** Line 1');
        // Continuation line is indented under child bullet
        expect(fileContent).toContain('\n    Line 2');
      });

      it('sanitizes HTML in child content', async () => {
        await createTestNote(ctx.vaultPath, 'child-sanitize.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child-sanitize.md',
            section: 'Log',
            content: 'Summary',
            suggestOutgoingLinks: false,
            children: [
              { label: '**Details:**', content: '<div>raw</div> text #tag %%hide%% ==hl==' },
            ],
          },
        });

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child-sanitize.md'), 'utf-8');
        expect(fileContent).toContain('&lt;div>raw</div>');
        expect(fileContent).toContain('&#35;tag');
        expect(fileContent).toContain('&#37;%hide&#37;%');
        expect(fileContent).toContain('&#61;=hl&#61;=');
      });

      it('handles code fence edge case in child content', async () => {
        await createTestNote(ctx.vaultPath, 'child-code.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child-code.md',
            section: 'Log',
            content: 'Snippet',
            suggestOutgoingLinks: false,
            children: [
              { label: '**Code:**', content: '```js\nconsole.log(1)\n```' },
            ],
          },
        });

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child-code.md'), 'utf-8');
        expect(fileContent).toContain('- Snippet');
        // Label is its own child bullet
        expect(fileContent).toContain('  - **Code:**');
        // Code fence lines are indented, not collapsed
        expect(fileContent).toContain('\n  ```js');
        expect(fileContent).toContain('\n    console.log(1)');
        expect(fileContent).toContain('\n  ```');
      });

      it('falls back to normal behavior when no children provided', async () => {
        await createTestNote(ctx.vaultPath, 'child-none.md', [
          '# Test',
          '',
          '## Log',
          '',
        ].join('\n'));

        await client.callTool({
          name: 'edit_section',
          arguments: {
            action: 'add',
            path: 'child-none.md',
            section: 'Log',
            content: 'Regular entry',
          },
        });

        const fileContent = readFileSync(path.join(ctx.vaultPath, 'child-none.md'), 'utf-8');
        expect(fileContent).toContain('Regular entry');
        expect(fileContent).not.toContain('  - **');
      });
    });
  });

  // ==========================================================================
  // note (action: 'create')
  // ==========================================================================
  describe('note (action: create)', () => {
    it('creates a note with valid path and content', async () => {
      const result = await client.callTool({
        name: 'note',
        arguments: {
          action: 'create',
          path: 'new-note.md',
          content: '# New Note\n\nHello world',
        },
      });

      expect(result.isError).toBeFalsy();
      const data = parseResult(result) as Record<string, unknown>;
      expect(data.success).toBe(true);

      const fileContent = readFileSync(path.join(ctx.vaultPath, 'new-note.md'), 'utf-8');
      expect(fileContent).toContain('Hello world');
    });

    it('blocks path traversal on create', async () => {
      const result = await client.callTool({
        name: 'note',
        arguments: {
          action: 'create',
          path: '../../outside.md',
          content: '# Exploit',
        },
      });

      const text = (result.content as Array<{ text: string }>)[0].text.toLowerCase();
      expect(text).toMatch(/path|blocked|invalid|outside|traversal|security/);
    });
  });

  // ==========================================================================
  // doctor (action: 'config')
  // ==========================================================================
  describe('doctor (action: config)', () => {
    it('returns valid config in get mode', async () => {
      const result = await client.callTool({
        name: 'doctor',
        arguments: { action: 'config', mode: 'get' },
      });

      expect(result.isError).toBeFalsy();
      const data = parseResult(result) as Record<string, unknown>;
      expect(data).toHaveProperty('wikilink_strictness');
    });

    it('persists a valid key with set mode', async () => {
      const setResult = await client.callTool({
        name: 'doctor',
        arguments: {
          action: 'config',
          mode: 'set',
          key: 'wikilink_strictness',
          value: 'conservative',
        },
      });

      expect(setResult.isError).toBeFalsy();
      const setData = parseResult(setResult) as Record<string, unknown>;
      expect(setData.wikilink_strictness).toBe('conservative');

      // Verify via get
      const getResult = await client.callTool({
        name: 'doctor',
        arguments: { action: 'config', mode: 'get' },
      });
      const getData = parseResult(getResult) as Record<string, unknown>;
      expect(getData.wikilink_strictness).toBe('conservative');
    });

    it('rejects an unknown config key', async () => {
      const result = await client.callTool({
        name: 'doctor',
        arguments: {
          action: 'config',
          mode: 'set',
          key: 'nonexistent',
          value: 'anything',
        },
      });

      const data = parseResult(result) as Record<string, unknown>;
      expect(data.error).toBeDefined();
      expect(String(data.error)).toContain('Unknown config key');
    });

    it('rejects an invalid value type', async () => {
      const result = await client.callTool({
        name: 'doctor',
        arguments: {
          action: 'config',
          mode: 'set',
          key: 'wikilink_strictness',
          value: 12345,
        },
      });

      const data = parseResult(result) as Record<string, unknown>;
      expect(data.error).toBeDefined();
      expect(String(data.error).toLowerCase()).toContain('invalid');
    });

    it('rejects read-only key (paths)', async () => {
      const result = await client.callTool({
        name: 'doctor',
        arguments: {
          action: 'config',
          mode: 'set',
          key: 'paths',
          value: {},
        },
      });

      const data = parseResult(result) as Record<string, unknown>;
      expect(data.error).toBeDefined();
      expect(String(data.error)).toContain('Unknown config key');
    });
  });

  // ==========================================================================
  // vault_undo_last_mutation
  // ==========================================================================
  describe('vault_undo_last_mutation', () => {
    it('reports not-a-git-repo in a temp vault', async () => {
      const result = await client.callTool({
        name: 'vault_undo_last_mutation',
        arguments: { confirm: true },
      });

      const text = (result.content as Array<{ text: string }>)[0].text.toLowerCase();
      expect(text).toMatch(/git|not.*repo|undo|failed|error/);
    });
  });
});
