import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AutoModeService } from '@/services/auto-mode-service.js';
import { ProviderFactory } from '@/providers/provider-factory.js';
import { FeatureLoader } from '@/services/feature-loader.js';
import {
  createTestGitRepo,
  createTestFeature,
  listBranches,
  listWorktrees,
  branchExists,
  worktreeExists,
  type TestRepo,
} from '../helpers/git-test-repo.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

vi.mock('@/providers/provider-factory.js');

describe('auto-mode-service.ts (integration)', () => {
  let service: AutoModeService;
  let testRepo: TestRepo;
  let featureLoader: FeatureLoader;
  const mockEvents = {
    subscribe: vi.fn(),
    emit: vi.fn(),
  };

  beforeEach(async () => {
    vi.clearAllMocks();
    service = new AutoModeService(mockEvents as any);
    featureLoader = new FeatureLoader();
    testRepo = await createTestGitRepo();
  });

  afterEach(async () => {
    // Stop any running auto loops
    await service.stopAutoLoop();

    // Cleanup test repo
    if (testRepo) {
      await testRepo.cleanup();
    }
  });

  describe('worktree operations', () => {
    it('should use existing git worktree for feature', async () => {
      const branchName = 'feature/test-feature-1';

      // Create a test feature with branchName set
      await createTestFeature(testRepo.path, 'test-feature-1', {
        id: 'test-feature-1',
        category: 'test',
        description: 'Test feature',
        status: 'pending',
        branchName: branchName,
      });

      // Create worktree before executing (worktrees are now created when features are added/edited)
      const worktreesDir = path.join(testRepo.path, '.worktrees');
      const worktreePath = path.join(worktreesDir, 'test-feature-1');
      await fs.mkdir(worktreesDir, { recursive: true });
      await execAsync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
        cwd: testRepo.path,
      });

      // Mock provider to complete quickly
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [{ type: 'text', text: 'Feature implemented' }],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Execute feature with worktrees enabled
      await service.executeFeature(
        testRepo.path,
        'test-feature-1',
        true, // useWorktrees
        false // isAutoMode
      );

      // Verify branch exists (was created when worktree was created)
      const branches = await listBranches(testRepo.path);
      expect(branches).toContain(branchName);

      // Verify worktree exists and is being used
      // The service should have found and used the worktree (check via logs)
      // We can verify the worktree exists by checking git worktree list
      const worktrees = await listWorktrees(testRepo.path);
      expect(worktrees.length).toBeGreaterThan(0);
      // Verify that at least one worktree path contains our feature ID
      const worktreePathsMatch = worktrees.some(
        (wt) => wt.includes('test-feature-1') || wt.includes('.worktrees')
      );
      expect(worktreePathsMatch).toBe(true);

      // Note: Worktrees are not automatically cleaned up by the service
      // This is expected behavior - manual cleanup is required
    }, 30000);

    it('should handle error gracefully', async () => {
      await createTestFeature(testRepo.path, 'test-feature-error', {
        id: 'test-feature-error',
        category: 'test',
        description: 'Test feature that errors',
        status: 'pending',
      });

      // Mock provider that throws error
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          throw new Error('Provider error');
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Execute feature (should handle error)
      await service.executeFeature(testRepo.path, 'test-feature-error', true, false);

      // Verify feature status was updated to backlog (error status)
      const feature = await featureLoader.get(testRepo.path, 'test-feature-error');
      expect(feature?.status).toBe('backlog');
    }, 30000);

    it('should work without worktrees', async () => {
      await createTestFeature(testRepo.path, 'test-no-worktree', {
        id: 'test-no-worktree',
        category: 'test',
        description: 'Test without worktree',
        status: 'pending',
        skipTests: true,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Execute without worktrees
      await service.executeFeature(
        testRepo.path,
        'test-no-worktree',
        false, // useWorktrees = false
        false
      );

      // Feature should be updated successfully
      const feature = await featureLoader.get(testRepo.path, 'test-no-worktree');
      expect(feature?.status).toBe('waiting_approval');
    }, 30000);
  });

  describe('feature execution', () => {
    it('should execute feature and update status', async () => {
      await createTestFeature(testRepo.path, 'feature-exec-1', {
        id: 'feature-exec-1',
        category: 'ui',
        description: 'Execute this feature',
        status: 'pending',
        skipTests: true,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [{ type: 'text', text: 'Implemented the feature' }],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(
        testRepo.path,
        'feature-exec-1',
        false, // Don't use worktrees so agent output is saved to main project
        false
      );

      // Check feature status was updated
      const feature = await featureLoader.get(testRepo.path, 'feature-exec-1');
      expect(feature?.status).toBe('waiting_approval');

      // Check agent output was saved
      const agentOutput = await featureLoader.getAgentOutput(testRepo.path, 'feature-exec-1');
      expect(agentOutput).toBeTruthy();
      expect(agentOutput).toContain('Implemented the feature');
    }, 30000);

    it('should handle feature not found', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Try to execute non-existent feature
      await service.executeFeature(testRepo.path, 'nonexistent-feature', true, false);

      // Should emit error event
      expect(mockEvents.emit).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          featureId: 'nonexistent-feature',
          error: expect.stringContaining('not found'),
        })
      );
    }, 30000);

    it('should prevent duplicate feature execution', async () => {
      await createTestFeature(testRepo.path, 'feature-dup', {
        id: 'feature-dup',
        category: 'test',
        description: 'Duplicate test',
        status: 'pending',
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          // Simulate slow execution
          await new Promise((resolve) => setTimeout(resolve, 500));
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Start first execution
      const promise1 = service.executeFeature(testRepo.path, 'feature-dup', false, false);

      // Try to start second execution (should throw)
      await expect(
        service.executeFeature(testRepo.path, 'feature-dup', false, false)
      ).rejects.toThrow('already running');

      await promise1;
    }, 30000);

    it('should use feature-specific model', async () => {
      await createTestFeature(testRepo.path, 'feature-model', {
        id: 'feature-model',
        category: 'test',
        description: 'Model test',
        status: 'pending',
        model: 'claude-sonnet-4-20250514',
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(testRepo.path, 'feature-model', false, false);

      // Should have used claude-sonnet-4-20250514
      expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
    }, 30000);
  });

  describe('auto loop', () => {
    it('should start and stop auto loop', async () => {
      const startPromise = service.startAutoLoop(testRepo.path, 2);

      // Give it time to start
      await new Promise((resolve) => setTimeout(resolve, 100));

      // Stop the loop
      const runningCount = await service.stopAutoLoop();

      expect(runningCount).toBe(0);
      await startPromise.catch(() => {}); // Cleanup
    }, 10000);

    it('should process pending features in auto loop', async () => {
      // Create multiple pending features
      await createTestFeature(testRepo.path, 'auto-1', {
        id: 'auto-1',
        category: 'test',
        description: 'Auto feature 1',
        status: 'pending',
        skipTests: true,
      });

      await createTestFeature(testRepo.path, 'auto-2', {
        id: 'auto-2',
        category: 'test',
        description: 'Auto feature 2',
        status: 'pending',
        skipTests: true,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Start auto loop
      const startPromise = service.startAutoLoop(testRepo.path, 2);

      // Wait for features to be processed
      await new Promise((resolve) => setTimeout(resolve, 3000));

      // Stop the loop
      await service.stopAutoLoop();
      await startPromise.catch(() => {});

      // Check that features were updated
      const feature1 = await featureLoader.get(testRepo.path, 'auto-1');
      const feature2 = await featureLoader.get(testRepo.path, 'auto-2');

      // At least one should have been processed
      const processedCount = [feature1, feature2].filter(
        (f) => f?.status === 'waiting_approval' || f?.status === 'in_progress'
      ).length;

      expect(processedCount).toBeGreaterThan(0);
    }, 15000);

    it('should respect max concurrency', async () => {
      // Create 5 features
      for (let i = 1; i <= 5; i++) {
        await createTestFeature(testRepo.path, `concurrent-${i}`, {
          id: `concurrent-${i}`,
          category: 'test',
          description: `Concurrent feature ${i}`,
          status: 'pending',
        });
      }

      let concurrentCount = 0;
      let maxConcurrent = 0;

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          concurrentCount++;
          maxConcurrent = Math.max(maxConcurrent, concurrentCount);

          // Simulate work
          await new Promise((resolve) => setTimeout(resolve, 500));

          concurrentCount--;

          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Start with max concurrency of 2
      const startPromise = service.startAutoLoop(testRepo.path, 2);

      // Wait for some features to be processed
      await new Promise((resolve) => setTimeout(resolve, 3000));

      await service.stopAutoLoop();
      await startPromise.catch(() => {});

      // Max concurrent should not exceed 2
      expect(maxConcurrent).toBeLessThanOrEqual(2);
    }, 15000);

    it('should emit auto mode events', async () => {
      const startPromise = service.startAutoLoop(testRepo.path, 1);

      // Wait for start event
      await new Promise((resolve) => setTimeout(resolve, 100));

      // Check start event was emitted
      const startEvent = mockEvents.emit.mock.calls.find((call) =>
        call[1]?.message?.includes('Auto mode started')
      );
      expect(startEvent).toBeTruthy();

      await service.stopAutoLoop();
      await startPromise.catch(() => {});

      // Check stop event was emitted (emitted immediately by stopAutoLoop)
      const stopEvent = mockEvents.emit.mock.calls.find(
        (call) =>
          call[1]?.type === 'auto_mode_stopped' || call[1]?.message?.includes('Auto mode stopped')
      );
      expect(stopEvent).toBeTruthy();
    }, 10000);
  });

  describe('error handling', () => {
    it('should handle provider errors gracefully', async () => {
      await createTestFeature(testRepo.path, 'error-feature', {
        id: 'error-feature',
        category: 'test',
        description: 'Error test',
        status: 'pending',
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          throw new Error('Provider execution failed');
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      // Should not throw
      await service.executeFeature(testRepo.path, 'error-feature', true, false);

      // Feature should be marked as backlog (error status)
      const feature = await featureLoader.get(testRepo.path, 'error-feature');
      expect(feature?.status).toBe('backlog');
    }, 30000);

    it('should continue auto loop after feature error', async () => {
      await createTestFeature(testRepo.path, 'fail-1', {
        id: 'fail-1',
        category: 'test',
        description: 'Will fail',
        status: 'pending',
      });

      await createTestFeature(testRepo.path, 'success-1', {
        id: 'success-1',
        category: 'test',
        description: 'Will succeed',
        status: 'pending',
      });

      let callCount = 0;
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          callCount++;
          if (callCount === 1) {
            throw new Error('First feature fails');
          }
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      const startPromise = service.startAutoLoop(testRepo.path, 1);

      // Wait for both features to be attempted
      await new Promise((resolve) => setTimeout(resolve, 5000));

      await service.stopAutoLoop();
      await startPromise.catch(() => {});

      // Both features should have been attempted
      expect(callCount).toBeGreaterThanOrEqual(1);
    }, 15000);
  });

  describe('planning mode', () => {
    it('should execute feature with skip planning mode', async () => {
      await createTestFeature(testRepo.path, 'skip-plan-feature', {
        id: 'skip-plan-feature',
        category: 'test',
        description: 'Feature with skip planning',
        status: 'pending',
        planningMode: 'skip',
        skipTests: true,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [{ type: 'text', text: 'Feature implemented' }],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(testRepo.path, 'skip-plan-feature', false, false);

      const feature = await featureLoader.get(testRepo.path, 'skip-plan-feature');
      expect(feature?.status).toBe('waiting_approval');
    }, 30000);

    it('should execute feature with lite planning mode without approval', async () => {
      await createTestFeature(testRepo.path, 'lite-plan-feature', {
        id: 'lite-plan-feature',
        category: 'test',
        description: 'Feature with lite planning',
        status: 'pending',
        planningMode: 'lite',
        requirePlanApproval: false,
        skipTests: true,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [
                {
                  type: 'text',
                  text: '[PLAN_GENERATED] Planning outline complete.\n\nFeature implemented',
                },
              ],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(testRepo.path, 'lite-plan-feature', false, false);

      const feature = await featureLoader.get(testRepo.path, 'lite-plan-feature');
      expect(feature?.status).toBe('waiting_approval');
    }, 30000);

    it('should emit planning_started event for spec mode', async () => {
      await createTestFeature(testRepo.path, 'spec-plan-feature', {
        id: 'spec-plan-feature',
        category: 'test',
        description: 'Feature with spec planning',
        status: 'pending',
        planningMode: 'spec',
        requirePlanApproval: false,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [
                { type: 'text', text: 'Spec generated\n\n[SPEC_GENERATED] Review the spec.' },
              ],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(testRepo.path, 'spec-plan-feature', false, false);

      // Check planning_started event was emitted
      const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'spec');
      expect(planningEvent).toBeTruthy();
    }, 30000);

    it('should handle feature with full planning mode', async () => {
      await createTestFeature(testRepo.path, 'full-plan-feature', {
        id: 'full-plan-feature',
        category: 'test',
        description: 'Feature with full planning',
        status: 'pending',
        planningMode: 'full',
        requirePlanApproval: false,
      });

      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [
                { type: 'text', text: 'Full spec with phases\n\n[SPEC_GENERATED] Review.' },
              ],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      await service.executeFeature(testRepo.path, 'full-plan-feature', false, false);

      // Check planning_started event was emitted with full mode
      const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'full');
      expect(planningEvent).toBeTruthy();
    }, 30000);

    it('should track pending approval correctly', async () => {
      // Initially no pending approvals
      expect(service.hasPendingApproval('non-existent')).toBe(false);
    });

    it('should cancel pending approval gracefully', () => {
      // Should not throw when cancelling non-existent approval
      expect(() => service.cancelPlanApproval('non-existent')).not.toThrow();
    });

    it('should resolve approval with error for non-existent feature', async () => {
      const result = await service.resolvePlanApproval(
        'non-existent',
        true,
        undefined,
        undefined,
        undefined
      );
      expect(result.success).toBe(false);
      expect(result.error).toContain('No pending approval');
    });
  });
});
