/**
 * SimpleGitWorktreeManager Tests
 *
 * Comprehensive tests for the WorktreeManager adapter.
 * Uses temporary git repositories for each test.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { simpleGit } from 'simple-git';
import { SimpleGitWorktreeManager } from './manager.js';
import { EventEmitterBus } from '../events/bus.js';
import type { EventBus } from '../events/types.js';

/**
 * Create a temporary git repository for testing.
 * Returns the path to the repo and a cleanup function.
 */
async function createTestRepo(): Promise<{
  repoPath: string;
  cleanup: () => Promise<void>;
}> {
  // Create temp directory
  const repoPath = await mkdtemp(path.join(tmpdir(), 'cw-test-repo-'));

  // Initialize git repo
  const git = simpleGit(repoPath);
  await git.init();

  // Configure git user for commits
  await git.addConfig('user.email', 'test@example.com');
  await git.addConfig('user.name', 'Test User');

  // Create initial commit (required for worktrees)
  await writeFile(path.join(repoPath, 'README.md'), '# Test Repo\n');
  await git.add('README.md');
  await git.commit('Initial commit');

  return {
    repoPath,
    cleanup: async () => {
      // Force remove temp directory
      await rm(repoPath, { recursive: true, force: true });
    },
  };
}

describe('SimpleGitWorktreeManager', () => {
  let repoPath: string;
  let cleanup: () => Promise<void>;
  let manager: SimpleGitWorktreeManager;
  let eventBus: EventBus;
  let emittedEvents: Array<{ type: string; payload: unknown }>;

  beforeEach(async () => {
    const testRepo = await createTestRepo();
    repoPath = testRepo.repoPath;
    cleanup = testRepo.cleanup;

    // Create event bus and track emitted events
    eventBus = new EventEmitterBus();
    emittedEvents = [];

    // Subscribe to all worktree events
    eventBus.on('worktree:created', (e) =>
      emittedEvents.push({ type: e.type, payload: e.payload })
    );
    eventBus.on('worktree:removed', (e) =>
      emittedEvents.push({ type: e.type, payload: e.payload })
    );
    eventBus.on('worktree:merged', (e) =>
      emittedEvents.push({ type: e.type, payload: e.payload })
    );
    eventBus.on('worktree:conflict', (e) =>
      emittedEvents.push({ type: e.type, payload: e.payload })
    );

    manager = new SimpleGitWorktreeManager(repoPath, eventBus);
  });

  afterEach(async () => {
    await cleanup();
  });

  // ==========================================================================
  // CRUD Operations Tests
  // ==========================================================================

  describe('create', () => {
    it('creates worktree at expected path', async () => {
      const worktree = await manager.create('test-wt', 'feature/test');

      expect(worktree.id).toBe('test-wt');
      expect(worktree.branch).toBe('feature/test');
      expect(worktree.path).toContain('.cw-worktrees');
      expect(worktree.path).toContain('test-wt');
      expect(worktree.isMainWorktree).toBe(false);
    });

    it('creates branch from specified base', async () => {
      // Create a second branch first
      const git = simpleGit(repoPath);
      await git.checkoutLocalBranch('develop');
      await writeFile(path.join(repoPath, 'develop.txt'), 'develop content\n');
      await git.add('develop.txt');
      await git.commit('Develop commit');
      await git.checkout('main');

      // Create worktree from develop branch
      const worktree = await manager.create(
        'from-develop',
        'feature/from-dev',
        'develop'
      );

      expect(worktree.branch).toBe('feature/from-dev');

      // Verify the worktree has the develop.txt file
      const worktreeGit = simpleGit(worktree.path);
      const files = await worktreeGit.raw(['ls-files']);
      expect(files).toContain('develop.txt');
    });

    it('emits worktree:created event', async () => {
      await manager.create('evt-test', 'feature/event');

      const createdEvents = emittedEvents.filter(
        (e) => e.type === 'worktree:created'
      );
      expect(createdEvents.length).toBe(1);
      expect(createdEvents[0].payload).toEqual({
        worktreeId: 'evt-test',
        branch: 'feature/event',
        path: expect.stringContaining('evt-test'),
      });
    });
  });

  describe('remove', () => {
    it('removes worktree directory', async () => {
      const worktree = await manager.create('to-remove', 'feature/remove');

      // Verify worktree exists
      let worktrees = await manager.list();
      const found = worktrees.find((wt) => wt.id === 'to-remove');
      expect(found).toBeDefined();

      // Remove worktree
      await manager.remove('to-remove');

      // Verify worktree is gone
      worktrees = await manager.list();
      const stillFound = worktrees.find((wt) => wt.id === 'to-remove');
      expect(stillFound).toBeUndefined();
    });

    it('emits worktree:removed event', async () => {
      await manager.create('remove-evt', 'feature/remove-evt');
      emittedEvents = []; // Clear creation event

      await manager.remove('remove-evt');

      const removedEvents = emittedEvents.filter(
        (e) => e.type === 'worktree:removed'
      );
      expect(removedEvents.length).toBe(1);
      expect(removedEvents[0].payload).toEqual({
        worktreeId: 'remove-evt',
        branch: 'feature/remove-evt',
      });
    });

    it('throws for non-existent worktree', async () => {
      await expect(manager.remove('non-existent')).rejects.toThrow(
        'Worktree not found: non-existent'
      );
    });
  });

  describe('list', () => {
    it('returns all worktrees', async () => {
      await manager.create('wt-1', 'feature/one');
      await manager.create('wt-2', 'feature/two');

      const worktrees = await manager.list();

      // Should have main + 2 created worktrees
      expect(worktrees.length).toBe(3);

      const ids = worktrees.map((wt) => wt.id);
      expect(ids).toContain('main');
      expect(ids).toContain('wt-1');
      expect(ids).toContain('wt-2');
    });

    it('marks main worktree correctly', async () => {
      await manager.create('not-main', 'feature/not-main');

      const worktrees = await manager.list();

      const mainWt = worktrees.find((wt) => wt.id === 'main');
      const otherWt = worktrees.find((wt) => wt.id === 'not-main');

      expect(mainWt?.isMainWorktree).toBe(true);
      expect(otherWt?.isMainWorktree).toBe(false);
    });
  });

  describe('get', () => {
    it('returns worktree by id', async () => {
      await manager.create('get-me', 'feature/get');

      const worktree = await manager.get('get-me');

      expect(worktree).not.toBeNull();
      expect(worktree!.id).toBe('get-me');
      expect(worktree!.branch).toBe('feature/get');
    });

    it('returns null for non-existent id', async () => {
      const worktree = await manager.get('does-not-exist');
      expect(worktree).toBeNull();
    });
  });

  // ==========================================================================
  // Diff Operations Tests
  // ==========================================================================

  describe('diff', () => {
    it('returns empty for clean worktree', async () => {
      const worktree = await manager.create('clean-wt', 'feature/clean');

      const diff = await manager.diff('clean-wt');

      expect(diff.files).toEqual([]);
      expect(diff.summary).toBe('No changes');
    });

    it('detects added files', async () => {
      const worktree = await manager.create('add-file', 'feature/add');

      // Add a new file in the worktree
      await writeFile(path.join(worktree.path, 'new-file.txt'), 'new content\n');

      // Stage the file
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('new-file.txt');

      const diff = await manager.diff('add-file');

      expect(diff.files.length).toBe(1);
      expect(diff.files[0]).toEqual({
        path: 'new-file.txt',
        status: 'added',
      });
    });

    it('detects modified files', async () => {
      const worktree = await manager.create('mod-file', 'feature/mod');

      // Modify existing README.md
      await writeFile(
        path.join(worktree.path, 'README.md'),
        '# Modified Content\n'
      );

      const diff = await manager.diff('mod-file');

      expect(diff.files.length).toBe(1);
      expect(diff.files[0]).toEqual({
        path: 'README.md',
        status: 'modified',
      });
    });

    it('detects deleted files', async () => {
      const worktree = await manager.create('del-file', 'feature/del');

      // Delete README.md using git rm
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.rm('README.md');

      const diff = await manager.diff('del-file');

      expect(diff.files.length).toBe(1);
      expect(diff.files[0]).toEqual({
        path: 'README.md',
        status: 'deleted',
      });
    });

    it('throws for non-existent worktree', async () => {
      await expect(manager.diff('no-such-wt')).rejects.toThrow(
        'Worktree not found: no-such-wt'
      );
    });
  });

  // ==========================================================================
  // Merge Operations Tests
  // ==========================================================================

  describe('merge', () => {
    it('succeeds with clean merge', async () => {
      const worktree = await manager.create('merge-clean', 'feature/merge');

      // Add a new file in the worktree
      await writeFile(
        path.join(worktree.path, 'feature.txt'),
        'feature content\n'
      );
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('feature.txt');
      await worktreeGit.commit('Add feature');

      // Merge back to main
      const result = await manager.merge('merge-clean', 'main');

      expect(result.success).toBe(true);
      expect(result.message).toBe('Merged successfully');
      expect(result.conflicts).toBeUndefined();
    });

    it('emits worktree:merged event on success', async () => {
      const worktree = await manager.create('merge-evt', 'feature/merge-evt');

      // Add a file and commit
      await writeFile(
        path.join(worktree.path, 'merge-evt.txt'),
        'merge event test\n'
      );
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('merge-evt.txt');
      await worktreeGit.commit('Add merge event file');

      emittedEvents = []; // Clear creation event
      await manager.merge('merge-evt', 'main');

      const mergedEvents = emittedEvents.filter(
        (e) => e.type === 'worktree:merged'
      );
      expect(mergedEvents.length).toBe(1);
      expect(mergedEvents[0].payload).toEqual({
        worktreeId: 'merge-evt',
        sourceBranch: 'feature/merge-evt',
        targetBranch: 'main',
      });
    });

    it('detects conflicts and returns them', async () => {
      // Create a worktree
      const worktree = await manager.create(
        'conflict-wt',
        'feature/conflict'
      );

      // Modify README.md in the worktree
      await writeFile(
        path.join(worktree.path, 'README.md'),
        '# Worktree Change\n'
      );
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('README.md');
      await worktreeGit.commit('Worktree change');

      // Also modify README.md in main (create conflict)
      const mainGit = simpleGit(repoPath);
      await writeFile(path.join(repoPath, 'README.md'), '# Main Change\n');
      await mainGit.add('README.md');
      await mainGit.commit('Main change');

      // Try to merge - should detect conflict
      const result = await manager.merge('conflict-wt', 'main');

      expect(result.success).toBe(false);
      expect(result.message).toBe('Merge conflicts detected');
      expect(result.conflicts).toBeDefined();
      expect(result.conflicts).toContain('README.md');
    });

    it('emits worktree:conflict event on conflict', async () => {
      // Set up conflict scenario
      const worktree = await manager.create(
        'conflict-evt',
        'feature/conflict-evt'
      );

      // Modify README.md in worktree
      await writeFile(
        path.join(worktree.path, 'README.md'),
        '# Worktree Version\n'
      );
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('README.md');
      await worktreeGit.commit('Worktree version');

      // Modify README.md in main
      const mainGit = simpleGit(repoPath);
      await writeFile(path.join(repoPath, 'README.md'), '# Main Version\n');
      await mainGit.add('README.md');
      await mainGit.commit('Main version');

      emittedEvents = []; // Clear creation event
      await manager.merge('conflict-evt', 'main');

      const conflictEvents = emittedEvents.filter(
        (e) => e.type === 'worktree:conflict'
      );
      expect(conflictEvents.length).toBe(1);
      expect(conflictEvents[0].payload).toEqual({
        worktreeId: 'conflict-evt',
        sourceBranch: 'feature/conflict-evt',
        targetBranch: 'main',
        conflictingFiles: ['README.md'],
      });
    });

    it('aborts and cleans up after conflict', async () => {
      // Set up conflict scenario
      const worktree = await manager.create('abort-wt', 'feature/abort');

      // Create conflict
      await writeFile(
        path.join(worktree.path, 'README.md'),
        '# Worktree Abort\n'
      );
      const worktreeGit = simpleGit(worktree.path);
      await worktreeGit.add('README.md');
      await worktreeGit.commit('Worktree abort');

      const mainGit = simpleGit(repoPath);
      await writeFile(path.join(repoPath, 'README.md'), '# Main Abort\n');
      await mainGit.add('README.md');
      await mainGit.commit('Main abort');

      // Merge should fail with conflicts
      await manager.merge('abort-wt', 'main');

      // Verify repo has no merge conflicts (merge was aborted)
      const status = await mainGit.status();
      expect(status.conflicted.length).toBe(0);
      // The repo should not be in a merging state
      expect(status.current).toBe('main');
    });

    it('throws for non-existent worktree', async () => {
      await expect(manager.merge('no-wt', 'main')).rejects.toThrow(
        'Worktree not found: no-wt'
      );
    });
  });

  // ==========================================================================
  // Edge Cases
  // ==========================================================================

  describe('edge cases', () => {
    it('works without eventBus', async () => {
      // Create manager without eventBus
      const managerNoEvents = new SimpleGitWorktreeManager(repoPath);

      // Should not throw
      const worktree = await managerNoEvents.create(
        'no-events',
        'feature/no-events'
      );
      expect(worktree.id).toBe('no-events');

      await managerNoEvents.remove('no-events');
      // No assertions needed - just verifying no errors
    });

    it('handles worktree with uncommitted changes on remove', async () => {
      const worktree = await manager.create(
        'uncommitted',
        'feature/uncommitted'
      );

      // Add uncommitted changes
      await writeFile(
        path.join(worktree.path, 'uncommitted.txt'),
        'not committed\n'
      );

      // Remove should work with --force
      await manager.remove('uncommitted');

      const remaining = await manager.list();
      const found = remaining.find((wt) => wt.id === 'uncommitted');
      expect(found).toBeUndefined();
    });
  });
});
