import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FeatureLoader } from '@/services/feature-loader.js';
import * as fs from 'fs/promises';
import path from 'path';

vi.mock('fs/promises');

describe('feature-loader.ts', () => {
  let loader: FeatureLoader;
  const testProjectPath = '/test/project';

  beforeEach(() => {
    vi.clearAllMocks();
    loader = new FeatureLoader();
  });

  describe('getFeaturesDir', () => {
    it('should return features directory path', () => {
      const result = loader.getFeaturesDir(testProjectPath);
      expect(result).toContain('test');
      expect(result).toContain('project');
      expect(result).toContain('.automaker');
      expect(result).toContain('features');
    });
  });

  describe('getFeatureImagesDir', () => {
    it('should return feature images directory path', () => {
      const result = loader.getFeatureImagesDir(testProjectPath, 'feature-123');
      expect(result).toContain('features');
      expect(result).toContain('feature-123');
      expect(result).toContain('images');
    });
  });

  describe('getFeatureDir', () => {
    it('should return feature directory path', () => {
      const result = loader.getFeatureDir(testProjectPath, 'feature-123');
      expect(result).toContain('features');
      expect(result).toContain('feature-123');
    });
  });

  describe('getFeatureJsonPath', () => {
    it('should return feature.json path', () => {
      const result = loader.getFeatureJsonPath(testProjectPath, 'feature-123');
      expect(result).toContain('features');
      expect(result).toContain('feature-123');
      expect(result).toContain('feature.json');
    });
  });

  describe('getAgentOutputPath', () => {
    it('should return agent-output.md path', () => {
      const result = loader.getAgentOutputPath(testProjectPath, 'feature-123');
      expect(result).toContain('features');
      expect(result).toContain('feature-123');
      expect(result).toContain('agent-output.md');
    });
  });

  describe('generateFeatureId', () => {
    it('should generate unique feature ID with timestamp', () => {
      const id1 = loader.generateFeatureId();
      const id2 = loader.generateFeatureId();

      expect(id1).toMatch(/^feature-\d+-[a-z0-9]+$/);
      expect(id2).toMatch(/^feature-\d+-[a-z0-9]+$/);
      expect(id1).not.toBe(id2);
    });

    it("should start with 'feature-'", () => {
      const id = loader.generateFeatureId();
      expect(id).toMatch(/^feature-/);
    });
  });

  describe('getAll', () => {
    it("should return empty array when features directory doesn't exist", async () => {
      vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));

      const result = await loader.getAll(testProjectPath);

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

    it('should load all features from feature directories', async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readdir).mockResolvedValue([
        { name: 'feature-1', isDirectory: () => true } as any,
        { name: 'feature-2', isDirectory: () => true } as any,
        { name: 'file.txt', isDirectory: () => false } as any,
      ]);

      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-1',
            category: 'ui',
            description: 'Feature 1',
          })
        )
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-2',
            category: 'backend',
            description: 'Feature 2',
          })
        );

      const result = await loader.getAll(testProjectPath);

      expect(result).toHaveLength(2);
      expect(result[0].id).toBe('feature-1');
      expect(result[1].id).toBe('feature-2');
    });

    it('should skip features without id field', async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readdir).mockResolvedValue([
        { name: 'feature-1', isDirectory: () => true } as any,
        { name: 'feature-2', isDirectory: () => true } as any,
      ]);

      const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(
          JSON.stringify({
            category: 'ui',
            description: 'Missing ID',
          })
        )
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-2',
            category: 'backend',
            description: 'Feature 2',
          })
        );

      const result = await loader.getAll(testProjectPath);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('feature-2');
      expect(consoleSpy).toHaveBeenCalledWith(
        '[FeatureLoader]',
        expect.stringContaining("missing required 'id' field")
      );

      consoleSpy.mockRestore();
    });

    it('should skip features with missing feature.json', async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readdir).mockResolvedValue([
        { name: 'feature-1', isDirectory: () => true } as any,
        { name: 'feature-2', isDirectory: () => true } as any,
      ]);

      const error: any = new Error('File not found');
      error.code = 'ENOENT';

      vi.mocked(fs.readFile)
        .mockRejectedValueOnce(error)
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-2',
            category: 'backend',
            description: 'Feature 2',
          })
        );

      const result = await loader.getAll(testProjectPath);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('feature-2');
    });

    it('should handle malformed JSON gracefully', async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readdir).mockResolvedValue([
        { name: 'feature-1', isDirectory: () => true } as any,
      ]);

      const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

      vi.mocked(fs.readFile).mockResolvedValue('invalid json{');

      const result = await loader.getAll(testProjectPath);

      expect(result).toEqual([]);
      expect(consoleSpy).toHaveBeenCalledWith(
        '[FeatureLoader]',
        expect.stringContaining('Failed to parse feature.json')
      );

      consoleSpy.mockRestore();
    });

    it('should sort features by creation order (timestamp)', async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readdir).mockResolvedValue([
        { name: 'feature-3', isDirectory: () => true } as any,
        { name: 'feature-1', isDirectory: () => true } as any,
        { name: 'feature-2', isDirectory: () => true } as any,
      ]);

      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-3000-xyz',
            category: 'ui',
          })
        )
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-1000-abc',
            category: 'ui',
          })
        )
        .mockResolvedValueOnce(
          JSON.stringify({
            id: 'feature-2000-def',
            category: 'ui',
          })
        );

      const result = await loader.getAll(testProjectPath);

      expect(result).toHaveLength(3);
      expect(result[0].id).toBe('feature-1000-abc');
      expect(result[1].id).toBe('feature-2000-def');
      expect(result[2].id).toBe('feature-3000-xyz');
    });
  });

  describe('get', () => {
    it('should return feature by ID', async () => {
      const featureData = {
        id: 'feature-123',
        category: 'ui',
        description: 'Test feature',
      };

      vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(featureData));

      const result = await loader.get(testProjectPath, 'feature-123');

      expect(result).toEqual(featureData);
    });

    it("should return null when feature doesn't exist", async () => {
      const error: any = new Error('File not found');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      const result = await loader.get(testProjectPath, 'feature-123');

      expect(result).toBeNull();
    });

    it('should throw on other errors', async () => {
      vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));

      await expect(loader.get(testProjectPath, 'feature-123')).rejects.toThrow('Permission denied');
    });
  });

  describe('create', () => {
    it('should create new feature', async () => {
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      const featureData = {
        category: 'ui',
        description: 'New feature',
      };

      const result = await loader.create(testProjectPath, featureData);

      expect(result).toMatchObject({
        category: 'ui',
        description: 'New feature',
        id: expect.stringMatching(/^feature-/),
      });
      expect(fs.writeFile).toHaveBeenCalled();
    });

    it('should use provided ID if given', async () => {
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      const result = await loader.create(testProjectPath, {
        id: 'custom-id',
        category: 'ui',
        description: 'Test',
      });

      expect(result.id).toBe('custom-id');
    });

    it('should set default category if not provided', async () => {
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      const result = await loader.create(testProjectPath, {
        description: 'Test',
      });

      expect(result.category).toBe('Uncategorized');
    });
  });

  describe('update', () => {
    it('should update existing feature', async () => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          id: 'feature-123',
          category: 'ui',
          description: 'Old description',
        })
      );
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      const result = await loader.update(testProjectPath, 'feature-123', {
        description: 'New description',
      });

      expect(result.description).toBe('New description');
      expect(result.category).toBe('ui');
      expect(fs.writeFile).toHaveBeenCalled();
    });

    it("should throw if feature doesn't exist", async () => {
      const error: any = new Error('File not found');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      await expect(loader.update(testProjectPath, 'feature-123', {})).rejects.toThrow('not found');
    });
  });

  describe('delete', () => {
    it('should delete feature directory', async () => {
      vi.mocked(fs.rm).mockResolvedValue(undefined);

      const result = await loader.delete(testProjectPath, 'feature-123');

      expect(result).toBe(true);
      expect(fs.rm).toHaveBeenCalledWith(expect.stringContaining('feature-123'), {
        recursive: true,
        force: true,
      });
    });

    it('should return false on error', async () => {
      vi.mocked(fs.rm).mockRejectedValue(new Error('Permission denied'));

      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

      const result = await loader.delete(testProjectPath, 'feature-123');

      expect(result).toBe(false);
      expect(consoleSpy).toHaveBeenCalledWith(
        '[FeatureLoader]',
        expect.stringContaining('Failed to delete feature'),
        expect.objectContaining({ message: 'Permission denied' })
      );
      consoleSpy.mockRestore();
    });
  });

  describe('getAgentOutput', () => {
    it('should return agent output content', async () => {
      vi.mocked(fs.readFile).mockResolvedValue('Agent output content');

      const result = await loader.getAgentOutput(testProjectPath, 'feature-123');

      expect(result).toBe('Agent output content');
    });

    it("should return null when file doesn't exist", async () => {
      const error: any = new Error('File not found');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      const result = await loader.getAgentOutput(testProjectPath, 'feature-123');

      expect(result).toBeNull();
    });

    it('should throw on other errors', async () => {
      vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));

      await expect(loader.getAgentOutput(testProjectPath, 'feature-123')).rejects.toThrow(
        'Permission denied'
      );
    });
  });

  describe('saveAgentOutput', () => {
    it('should save agent output to file', async () => {
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      await loader.saveAgentOutput(testProjectPath, 'feature-123', 'Output content');

      expect(fs.writeFile).toHaveBeenCalledWith(
        expect.stringContaining('agent-output.md'),
        'Output content',
        'utf-8'
      );
    });
  });

  describe('deleteAgentOutput', () => {
    it('should delete agent output file', async () => {
      vi.mocked(fs.unlink).mockResolvedValue(undefined);

      await loader.deleteAgentOutput(testProjectPath, 'feature-123');

      expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining('agent-output.md'));
    });

    it('should handle missing file gracefully', async () => {
      const error: any = new Error('File not found');
      error.code = 'ENOENT';
      vi.mocked(fs.unlink).mockRejectedValue(error);

      // Should not throw
      await expect(
        loader.deleteAgentOutput(testProjectPath, 'feature-123')
      ).resolves.toBeUndefined();
    });

    it('should throw on other errors', async () => {
      vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));

      await expect(loader.deleteAgentOutput(testProjectPath, 'feature-123')).rejects.toThrow(
        'Permission denied'
      );
    });
  });
});
