import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
  readMemoryIndex,
  parseMemoryEntry,
  serializeMemoryEntry,
  upsertMemoryEntry,
  deleteMemoryEntry,
  readMemoryEntry,
  repairMemoryIndex,
  MEMORY_TYPES,
} from './memory.js';
import { ensureUserFolder, userRoot } from './paths.js';

// ── Fixtures ──────────────────────────────────────────────────────────────────

let root: string;
const USER = 'test-user';

function memDir(): string {
  return join(userRoot(root, USER), 'memory');
}

function indexPath(): string {
  return join(memDir(), 'MEMORY.md');
}

// ── Setup ─────────────────────────────────────────────────────────────────────

beforeEach(() => {
  root = mkdtempSync(join(tmpdir(), 'mem-test-'));
  ensureUserFolder(root, USER);
});

afterEach(() => {
  rmSync(root, { recursive: true, force: true });
});

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('readMemoryIndex', () => {
  it('returns null when MEMORY.md is missing', () => {
    expect(readMemoryIndex(root, USER)).toBeNull();
  });

  it('returns null for an empty MEMORY.md', () => {
    writeFileSync(indexPath(), '');
    expect(readMemoryIndex(root, USER)).toBeNull();
  });

  it('returns content when MEMORY.md has entries', () => {
    const content = '- [foo](foo.md) — A test entry\n';
    writeFileSync(indexPath(), content);
    expect(readMemoryIndex(root, USER)).toBe(content);
  });

  it('truncates files larger than 32 KB and appends a notice', () => {
    // 33 KB of 'A'
    const big = Buffer.alloc(33 * 1024, 0x41).toString();
    writeFileSync(indexPath(), big);
    const result = readMemoryIndex(root, USER)!;
    expect(result).toContain('[truncated: original was');
    expect(result.length).toBeLessThan(33 * 1024 + 100);
  });

  it('returns null for invalid ownerId', () => {
    expect(readMemoryIndex(root, '../escape')).toBeNull();
  });
});

describe('parseMemoryEntry / serializeMemoryEntry', () => {
  it('roundtrip: serialize then parse returns the same entry', () => {
    const entry = {
      meta: { name: 'my-fact', description: 'A test fact', type: 'user' as const },
      body: 'Some body content here.',
    };
    const serialized = serializeMemoryEntry(entry);
    const parsed = parseMemoryEntry(serialized);
    expect(parsed.meta.name).toBe('my-fact');
    expect(parsed.meta.description).toBe('A test fact');
    expect(parsed.meta.type).toBe('user');
    expect(parsed.body.trim()).toBe('Some body content here.');
  });

  it('rejects an invalid type', () => {
    const src = '---\nname: bad\ndescription: test\ntype: invalid\n---\nbody\n';
    expect(() => parseMemoryEntry(src)).toThrow(/invalid memory type/);
  });

  it('accepts all four valid types', () => {
    for (const type of MEMORY_TYPES) {
      const src = `---\nname: n\ndescription: d\ntype: ${type}\n---\nbody\n`;
      const result = parseMemoryEntry(src);
      expect(result.meta.type).toBe(type);
    }
  });
});

describe('upsertMemoryEntry', () => {
  it('creates the fact file and updates the index', () => {
    const result = upsertMemoryEntry(root, USER, {
      name: 'my-fact',
      type: 'user',
      description: 'A test fact',
      body: 'This is the body.',
    });

    expect(existsSync(result.path)).toBe(true);
    expect(result.indexUpdated).toBe(true);

    const indexContent = readFileSync(indexPath(), 'utf-8');
    expect(indexContent).toContain('- [my-fact](my-fact.md) — A test fact');
  });

  it('replaces an existing entry (no duplicate index lines)', () => {
    upsertMemoryEntry(root, USER, {
      name: 'my-fact',
      type: 'user',
      description: 'First description',
      body: 'Body v1.',
    });
    upsertMemoryEntry(root, USER, {
      name: 'my-fact',
      type: 'feedback',
      description: 'Updated description',
      body: 'Body v2.',
    });

    const indexContent = readFileSync(indexPath(), 'utf-8');
    const lines = indexContent.split('\n').filter((l) => l.startsWith('- [my-fact]'));
    expect(lines).toHaveLength(1);
    expect(lines[0]).toContain('Updated description');

    // Verify file content was updated too
    const parsed = readMemoryEntry(root, USER, 'my-fact')!;
    expect(parsed.meta.type).toBe('feedback');
    expect(parsed.body.trim()).toBe('Body v2.');
  });

  it('preserves existing entries when upserting a new one', () => {
    upsertMemoryEntry(root, USER, { name: 'alpha', type: 'user', description: 'Alpha', body: 'A' });
    upsertMemoryEntry(root, USER, { name: 'beta', type: 'project', description: 'Beta', body: 'B' });

    const indexContent = readFileSync(indexPath(), 'utf-8');
    expect(indexContent).toContain('- [alpha](alpha.md) — Alpha');
    expect(indexContent).toContain('- [beta](beta.md) — Beta');
  });

  it('rejects an invalid type', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, {
        name: 'bad',
        type: 'invalid' as never,
        description: 'X',
        body: 'Y',
      }),
    ).toThrow(/invalid memory type/);
  });
});

describe('deleteMemoryEntry', () => {
  it('moves file to trash and removes index line', () => {
    upsertMemoryEntry(root, USER, { name: 'bye', type: 'reference', description: 'Bye', body: 'See ya.' });

    const deleted = deleteMemoryEntry(root, USER, 'bye');
    expect(deleted).toBe(true);

    // File should no longer exist in memory/
    expect(existsSync(join(memDir(), 'bye.md'))).toBe(false);

    // A file should be in trash/
    const trashDir = join(userRoot(root, USER), 'trash');
    const trashContents = readdirSync(trashDir);
    expect(trashContents.some((f) => f.includes('bye'))).toBe(true);

    // Index line should be removed
    const indexContent = readFileSync(indexPath(), 'utf-8');
    expect(indexContent).not.toContain('bye');
  });

  it('returns false when the entry does not exist', () => {
    expect(deleteMemoryEntry(root, USER, 'nonexistent')).toBe(false);
  });
});

describe('readMemoryEntry', () => {
  it('returns null when the entry does not exist', () => {
    expect(readMemoryEntry(root, USER, 'missing')).toBeNull();
  });

  it('loads an existing fact file', () => {
    upsertMemoryEntry(root, USER, {
      name: 'known-fact',
      type: 'project',
      description: 'A known fact',
      body: 'Known body.',
    });

    const entry = readMemoryEntry(root, USER, 'known-fact')!;
    expect(entry).not.toBeNull();
    expect(entry.meta.name).toBe('known-fact');
    expect(entry.meta.type).toBe('project');
    expect(entry.body.trim()).toBe('Known body.');
  });
});

describe('traversal-safe name handling', () => {
  it('upsertMemoryEntry with a simple name stays inside memory dir', () => {
    const result = upsertMemoryEntry(root, USER, {
      name: 'safe-name',
      type: 'user',
      description: 'Safe',
      body: 'ok',
    });
    expect(result.path.startsWith(memDir())).toBe(true);
  });
});

describe('assertValidMemoryName enforcement in helpers', () => {
  it('upsertMemoryEntry throws on traversal name "../escape"', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, { name: '../escape', type: 'user', description: 'x', body: 'y' })
    ).toThrow(/invalid memory name/);
  });

  it('upsertMemoryEntry throws on name with spaces', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, { name: 'has spaces', type: 'user', description: 'x', body: 'y' })
    ).toThrow(/invalid memory name/);
  });

  it('upsertMemoryEntry throws on name longer than 64 chars', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, { name: 'a'.repeat(100), type: 'user', description: 'x', body: 'y' })
    ).toThrow(/invalid memory name/);
  });

  it('deleteMemoryEntry throws on traversal name "../escape"', () => {
    expect(() => deleteMemoryEntry(root, USER, '../escape')).toThrow(/invalid memory name/);
  });

  it('readMemoryEntry throws on traversal name "../escape"', () => {
    expect(() => readMemoryEntry(root, USER, '../escape')).toThrow(/invalid memory name/);
  });

  it('upsertMemoryEntry throws when description contains newline', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, {
        name: 'valid',
        type: 'user',
        description: 'line1\nline2',
        body: 'ok',
      })
    ).toThrow(/single line/);
  });

  it('upsertMemoryEntry throws when description contains carriage return', () => {
    expect(() =>
      upsertMemoryEntry(root, USER, {
        name: 'valid',
        type: 'user',
        description: 'line1\rline2',
        body: 'ok',
      })
    ).toThrow(/single line/);
  });
});

describe('repairMemoryIndex (self-healing after a crashed upsert/delete)', () => {
  it('appends an index line for an orphan fact file', () => {
    // Simulate a half-completed upsert: fact file written, index update missed.
    const factPath = join(memDir(), 'orphan.md');
    writeFileSync(
      factPath,
      `---\nname: orphan\ndescription: was orphaned by a crash\ntype: project\n---\nbody\n`,
      'utf-8',
    );
    expect(existsSync(indexPath())).toBe(false);

    const result = repairMemoryIndex(root, USER);

    expect(result.added).toEqual(['orphan']);
    expect(result.removed).toEqual([]);
    const idx = readFileSync(indexPath(), 'utf-8');
    expect(idx).toContain('- [orphan](orphan.md) — was orphaned by a crash');
  });

  it('removes a dangling index line whose fact file is missing', () => {
    // Manually craft a stale index with a line pointing at a non-existent file.
    writeFileSync(
      indexPath(),
      '- [ghost](ghost.md) — referenced but missing\n- [stay](stay.md) — kept\n',
      'utf-8',
    );
    writeFileSync(
      join(memDir(), 'stay.md'),
      `---\nname: stay\ndescription: kept\ntype: user\n---\nbody\n`,
      'utf-8',
    );

    const result = repairMemoryIndex(root, USER);

    expect(result.removed).toEqual(['ghost']);
    expect(result.added).toEqual([]);
    const idx = readFileSync(indexPath(), 'utf-8');
    expect(idx).not.toContain('ghost');
    expect(idx).toContain('stay');
  });

  it('is a no-op when the index is already consistent', () => {
    upsertMemoryEntry(root, USER, { name: 'a', type: 'user', description: 'A', body: 'a' });
    const before = readFileSync(indexPath(), 'utf-8');

    const result = repairMemoryIndex(root, USER);

    expect(result).toEqual({ added: [], removed: [] });
    expect(readFileSync(indexPath(), 'utf-8')).toBe(before);
  });

  it('upsertMemoryEntry auto-heals a pre-existing orphan before applying its own write', () => {
    // Pre-create an orphan (fact file with no index line).
    writeFileSync(
      join(memDir(), 'left-over.md'),
      `---\nname: left-over\ndescription: from a previous crash\ntype: reference\n---\nbody\n`,
      'utf-8',
    );

    // Subsequent upsert (unrelated entry) should heal first, then add its own line.
    upsertMemoryEntry(root, USER, {
      name: 'new-one',
      type: 'project',
      description: 'fresh entry',
      body: 'fresh body',
    });

    const idx = readFileSync(indexPath(), 'utf-8');
    expect(idx).toContain('left-over');
    expect(idx).toContain('new-one');
  });

  it('deleteMemoryEntry auto-heals dangling index lines before applying its own change', () => {
    // Seed: real entry "alpha", dangling line "ghost".
    upsertMemoryEntry(root, USER, { name: 'alpha', type: 'user', description: 'A', body: 'a' });
    // Manually prepend a dangling line.
    const current = readFileSync(indexPath(), 'utf-8');
    writeFileSync(indexPath(), '- [ghost](ghost.md) — gone\n' + current, 'utf-8');

    deleteMemoryEntry(root, USER, 'alpha');

    const idx = existsSync(indexPath()) ? readFileSync(indexPath(), 'utf-8') : '';
    expect(idx).not.toContain('ghost');
    expect(idx).not.toContain('alpha');
  });

  it('ignores files that do not look like memory entries', () => {
    // Garbage files inside memory/ — should be left alone.
    writeFileSync(join(memDir(), 'scratch.txt'), 'not a memory entry', 'utf-8');
    writeFileSync(join(memDir(), '..hidden.md'), 'malformed', 'utf-8');

    const result = repairMemoryIndex(root, USER);

    expect(result.added).toEqual([]);
    expect(result.removed).toEqual([]);
  });
});
