import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import express, { Application } from 'express';
import request from 'supertest';
import { setupAgileAPI } from '../../api/agile.js';
import { AgileManager } from '../../../modules/agile-management/agile-manager.js';

// Mock the SQLite manager
vi.mock('../../../storage/sqlite-manager.js', () => ({
  getSQLiteManager: vi.fn(),
  ensureDatabaseReady: vi.fn()
}));

// Mock the AgileManager
vi.mock('../../../modules/agile-management/agile-manager.js', () => ({
  AgileManager: vi.fn()
}));

describe('Agile API', () => {
  let app: Application;
  let mockAgileManager: any;
  let mockSQLiteManager: any;

  beforeEach(async () => {
    app = express();
    app.use(express.json());

    // Setup mock SQLite manager
    mockSQLiteManager = {
      query: vi.fn(),
      get: vi.fn(),
      run: vi.fn(),
      all: vi.fn(), // Add the missing all method
      initialize: vi.fn(),
      close: vi.fn()
    };

    const { getSQLiteManager, ensureDatabaseReady } = await import('../../../storage/sqlite-manager.js');
    vi.mocked(getSQLiteManager).mockReturnValue(mockSQLiteManager as any);
    vi.mocked(ensureDatabaseReady).mockResolvedValue(mockSQLiteManager as any);

    // Create mock agile manager with all required methods
    mockAgileManager = {
      // Sprint methods
      getAllSprints: vi.fn(),
      getSprintById: vi.fn(),
      getActiveSprint: vi.fn(),
      
      // Story methods
      getAllStories: vi.fn(),
      getStoriesForSprint: vi.fn(),
      getStoriesBySprint: vi.fn(),
      getStoriesByStatus: vi.fn(),
      updateStory: vi.fn(),
      getBacklog: vi.fn(),
      
      // Epic methods
      getAllEpics: vi.fn(),
      getEpicById: vi.fn(),
      
      // Velocity and metrics
      calculateVelocity: vi.fn(),
      getBurndownData: vi.fn(),
      getSprintMetrics: vi.fn(),
      
      // Board management
      getBoards: vi.fn(),
      getBoardById: vi.fn(),
      updateBoard: vi.fn(),
      createBoard: vi.fn(),
      
      // Advanced features
      getBacklogItems: vi.fn(),
      prioritizeBacklog: vi.fn(),
      estimateStory: vi.fn(),
      getTeamCapacity: vi.fn(),
      generateSprintReport: vi.fn()
    };

    // Mock constructor
    (AgileManager as any).mockImplementation(() => mockAgileManager);

    // Setup the API
    setupAgileAPI(app, mockAgileManager);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('GET /api/agile/sprints', () => {
    it('should return all sprints', async () => {
      const mockSprintData = [
        {
          id: 'sprint-1',
          name: 'Sprint 1',
          goal: 'Complete MVP',
          status: 'active',
          start_date: new Date('2024-01-01').getTime(),
          end_date: new Date('2024-01-14').getTime(),
          duration: 14,
          team: JSON.stringify(['dev1', 'dev2']),
          story_points_planned: 20,
          story_points_completed: 15,
          stories_total: 5,
          stories_completed: 3,
          velocity: 15,
          created_at: Date.now(),
          updated_at: Date.now()
        }
      ];

      mockSQLiteManager.query.mockResolvedValue({
        success: true,
        data: mockSprintData
      });

      const response = await request(app).get('/api/agile/sprints');

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('sprints');
      expect(response.body.data.sprints).toHaveLength(1);
      expect(response.body.data.sprints[0].id).toBe('sprint-1');
      expect(response.body.data.sprints[0].name).toBe('Sprint 1');
    });

    it('should handle errors gracefully', async () => {
      mockSQLiteManager.query.mockResolvedValue({
        success: false,
        error: 'Database connection failed'
      });

      const response = await request(app).get('/api/agile/sprints');

      // API now returns 503 when database fails (no more mock fallbacks)
      expect(response.status).toBe(503);
      expect(response.body.success).toBe(false);
      expect(response.body.error).toBe('Database unavailable');
    });

    it('should return empty array when no sprints exist', async () => {
      mockSQLiteManager.query.mockResolvedValue({
        success: true,
        data: []
      });

      const response = await request(app).get('/api/agile/sprints');

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('sprints');
      expect(response.body.data.sprints).toEqual([]);
    });
  });

  describe('GET /api/agile/sprints/active', () => {
    it('should return the active sprint', async () => {
      const mockActiveSprint = {
        id: 'sprint-2',
        name: 'Sprint 2',
        status: 'active',
        goal: 'Implement user authentication',
        start_date: new Date('2024-01-01').getTime(),
        end_date: new Date('2024-01-14').getTime(),
        duration: 14,
        team: JSON.stringify(['dev1', 'dev2']),
        created_at: Date.now(),
        updated_at: Date.now()
      };

      mockSQLiteManager.get.mockResolvedValue({
        success: true,
        data: mockActiveSprint
      });

      const response = await request(app).get('/api/agile/sprints/active');

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.id).toBe('sprint-2');
      expect(response.body.data.name).toBe('Sprint 2');
    });

    it('should return 404 when no active sprint exists', async () => {
      mockSQLiteManager.get.mockResolvedValue({
        success: true,
        data: null
      });

      const response = await request(app).get('/api/agile/sprints/active');

      expect(response.status).toBe(404);
      expect(response.body.success).toBe(false);
      expect(response.body.error).toBe('No active sprint found');
    });
  });

  describe('GET /api/agile/sprints/:id', () => {
    it('should return a specific sprint with proper field mapping', async () => {
      // Mock raw database sprint data (snake_case as in DB)
      const mockDbSprint = {
        id: 'sprint-test-123',
        project_id: 'default',
        name: 'Test Sprint',
        goal: 'Complete test features',
        status: 'active',
        start_date: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
        end_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days from now
        duration: 14,
        team: '["Dev1", "Dev2"]',
        story_points_planned: 20,
        story_points_completed: 10,
        stories_total: 5,
        stories_completed: 2,
        velocity: 10,
        created_at: Date.now() - 10 * 24 * 60 * 60 * 1000,
        updated_at: Date.now()
      };

      // Mock the database queries
      mockSQLiteManager.get
        .mockResolvedValueOnce({ success: true, data: { count: 1 } }) // EXISTS check
        .mockResolvedValueOnce({ success: true, data: mockDbSprint }); // Sprint query

      mockSQLiteManager.all.mockResolvedValue({ 
        success: true, 
        data: [] // No stories for simplicity
      });

      mockSQLiteManager.run.mockResolvedValue({ success: true });

      const response = await request(app).get('/api/agile/sprints/sprint-test-123');

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toBeDefined();
      
      // Verify field mapping from snake_case to camelCase
      const sprint = response.body.data;
      expect(sprint.id).toBe('sprint-test-123');
      expect(sprint.name).toBe('Test Sprint');
      expect(sprint.status).toBe('active');
      expect(sprint.startDate).toBe(new Date(mockDbSprint.start_date).toISOString());
      expect(sprint.endDate).toBe(new Date(mockDbSprint.end_date).toISOString());
      expect(sprint.duration).toBe(14);
      expect(sprint.team).toEqual(['Dev1', 'Dev2']);
      expect(sprint.storyPointsPlanned).toBe(20);
      // These values are recalculated based on stories (which we mocked as empty)
      expect(sprint.storyPointsCompleted).toBe(0); // Recalculated from empty stories
      expect(sprint.storiesTotal).toBe(0); // Recalculated from empty stories
      expect(sprint.storiesCompleted).toBe(0); // Recalculated from empty stories
      expect(sprint.velocity).toBe(0); // Recalculated from completed points
    });

    it('should return 404 when sprint does not exist', async () => {
      // Mock database returning no sprint
      mockSQLiteManager.get
        .mockResolvedValueOnce({ success: true, data: { count: 0 } }) // EXISTS check
        .mockResolvedValueOnce({ success: true, data: undefined }); // No sprint found

      const response = await request(app).get('/api/agile/sprints/non-existent-sprint');

      expect(response.status).toBe(404);
      expect(response.body.success).toBe(false);
      expect(response.body.error).toBe('Sprint not found');
      expect(response.body.sprintId).toBe('non-existent-sprint');
    });

    it('should return 404 for various invalid sprint IDs', async () => {
      // Test various invalid sprint ID formats
      const invalidSprintIds = [
        'invalid-id',
        'sprint-999999',
        'SPRINT-UPPERCASE',
        'sprint_with_underscores',
        'sprint-' + 'x'.repeat(50), // Very long ID
        'sprint-<script>alert("xss")</script>', // XSS attempt
        'sprint-../../../etc/passwd', // Path traversal attempt
        'sprint-null',
        'sprint-undefined',
        ' ', // Whitespace
        'sprint-0',
        'sprint--double-dash',
        'sprint-!@#$%^&*()', // Special characters
      ];

      for (const invalidId of invalidSprintIds) {
        // Reset mocks for each test
        mockSQLiteManager.get.mockReset();
        mockSQLiteManager.get
          .mockResolvedValueOnce({ success: true, data: { count: 0 } }) // EXISTS check
          .mockResolvedValueOnce({ success: true, data: undefined }); // No sprint found

        const response = await request(app).get(`/api/agile/sprints/${encodeURIComponent(invalidId)}`);

        expect(response.status).toBe(404);
        expect(response.body.success).toBe(false);
        expect(response.body.error).toBe('Sprint not found');
        expect(response.body.sprintId).toBe(invalidId);
      }
    });

    it('should handle empty string ID by routing to list endpoint', async () => {
      // Test that empty string routes to the list endpoint
      // When ID is empty, the URL becomes /api/agile/sprints/ which matches the list endpoint
      mockSQLiteManager.query.mockResolvedValueOnce({
        success: true,
        data: []
      });

      const response = await request(app).get('/api/agile/sprints/');
      
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.sprints).toEqual([]);
    });

    it('should handle database errors gracefully', async () => {
      // Mock database error
      mockSQLiteManager.get.mockRejectedValue(new Error('Database connection failed'));

      const response = await request(app).get('/api/agile/sprints/sprint-1');

      expect(response.status).toBe(500);
      expect(response.body.success).toBe(false);
      expect(response.body.error).toBe('Failed to fetch sprint details');
    });

    it('should handle SQL injection attempts safely', async () => {
      // Test SQL injection attempts
      const sqlInjectionAttempts = [
        "'; DROP TABLE agile_sprints; --",
        "' OR '1'='1",
        "sprint-1' UNION SELECT * FROM users--",
        "sprint-1'; DELETE FROM agile_sprints WHERE '1'='1",
      ];

      for (const maliciousId of sqlInjectionAttempts) {
        mockSQLiteManager.get.mockReset();
        mockSQLiteManager.get
          .mockResolvedValueOnce({ success: true, data: { count: 0 } })
          .mockResolvedValueOnce({ success: true, data: undefined });

        const response = await request(app).get(`/api/agile/sprints/${encodeURIComponent(maliciousId)}`);

        expect(response.status).toBe(404);
        expect(response.body.success).toBe(false);
        expect(response.body.error).toBe('Sprint not found');
        
        // Verify the SQL query was called with the malicious ID as a parameter (safely)
        expect(mockSQLiteManager.get).toHaveBeenCalledWith(
          expect.stringContaining('WHERE id = ?'),
          [maliciousId]
        );
      }
    });

    // Regression test for the specific 404 bug
    it('should find sprints that exist in the list endpoint', async () => {
      // First, verify sprint exists in list
      const listData = [
        {
          id: 'sprint-bug-test',
          name: 'Bug Test Sprint',
          status: 'active',
          // ... other fields
        }
      ];

      mockSQLiteManager.query.mockResolvedValueOnce({ 
        success: true, 
        data: [{
          id: 'sprint-bug-test',
          name: 'Bug Test Sprint',
          status: 'active',
          start_date: Date.now(),
          end_date: Date.now() + 14 * 24 * 60 * 60 * 1000,
          team: '[]',
          story_points_planned: 0,
          story_points_completed: 0,
          stories_total: 0,
          stories_completed: 0,
          velocity: 0,
          created_at: Date.now(),
          updated_at: Date.now()
        }]
      });

      // Get list of sprints
      const listResponse = await request(app).get('/api/agile/sprints');
      expect(listResponse.status).toBe(200);
      expect(listResponse.body.data.sprints).toHaveLength(1);
      expect(listResponse.body.data.sprints[0].id).toBe('sprint-bug-test');

      // Now verify individual sprint endpoint finds the same sprint
      mockSQLiteManager.get
        .mockResolvedValueOnce({ success: true, data: { count: 1 } })
        .mockResolvedValueOnce({ 
          success: true, 
          data: {
            id: 'sprint-bug-test',
            name: 'Bug Test Sprint',
            status: 'active',
            start_date: Date.now(),
            end_date: Date.now() + 14 * 24 * 60 * 60 * 1000,
            team: '[]',
            story_points_planned: 0,
            story_points_completed: 0,
            stories_total: 0,
            stories_completed: 0,
            velocity: 0,
            created_at: Date.now(),
            updated_at: Date.now()
          }
        });

      mockSQLiteManager.all.mockResolvedValueOnce({ success: true, data: [] });
      mockSQLiteManager.run.mockResolvedValue({ success: true });

      // This should NOT return 404
      const individualResponse = await request(app).get('/api/agile/sprints/sprint-bug-test');
      expect(individualResponse.status).toBe(200);
      expect(individualResponse.body.success).toBe(true);
      expect(individualResponse.body.data.id).toBe('sprint-bug-test');
    });
  });

  describe('GET /api/agile/stories', () => {
    it('should return filtered stories', async () => {
      const mockStories = [
        {
          id: 'story-1',
          title: 'User login',
          status: 'in_progress',
          priority: 'high',
          storyPoints: 5
        },
        {
          id: 'story-2',
          title: 'User logout',
          status: 'done',
          priority: 'low',
          storyPoints: 3
        }
      ];

      mockAgileManager.getAllStories.mockResolvedValue(mockStories);

      const response = await request(app)
        .get('/api/agile/stories')
        .query({ status: 'in_progress', priority: 'high' });

      expect(response.status).toBe(200);
      expect(response.body.data).toHaveProperty('stories');
      expect(response.body.data.stories).toHaveLength(1);
      expect(response.body.data.stories[0]).toEqual(mockStories[0]);
      expect(mockAgileManager.getAllStories).toHaveBeenCalled();
    });

    it('should return all stories when no filters provided', async () => {
      const mockStories = [
        { id: 'story-1', title: 'Story 1' },
        { id: 'story-2', title: 'Story 2' }
      ];

      mockAgileManager.getAllStories.mockResolvedValue(mockStories);

      const response = await request(app).get('/api/agile/stories');

      expect(response.status).toBe(200);
      expect(response.body.data).toHaveProperty('stories');
      expect(response.body.data.stories).toEqual(mockStories);
      expect(mockAgileManager.getAllStories).toHaveBeenCalled();
    });
  });

  describe('PUT /api/agile/stories/:id', () => {
    it('should update a story', async () => {
      const updateData = {
        status: 'done',
        hoursSpent: 8
      };

      mockAgileManager.updateStory.mockResolvedValue({
        id: 'story-1',
        ...updateData
      });

      const response = await request(app)
        .put('/api/agile/stories/story-1')
        .send(updateData);

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(mockAgileManager.updateStory).toHaveBeenCalledWith('story-1', updateData);
    });
  });

  describe('GET /api/agile/velocity', () => {
    it('should return velocity metrics', async () => {
      // Mock completed sprints - returned in DESC order by end_date
      const mockSprints = [
        {
          id: 'sprint-2', 
          name: 'Sprint 2',
          status: 'completed',
          story_points_completed: 25,
          end_date: new Date('2024-01-28').getTime() / 1000
        },
        {
          id: 'sprint-1',
          name: 'Sprint 1',
          status: 'completed',
          story_points_completed: 20,
          end_date: new Date('2024-01-14').getTime() / 1000
        }
      ];

      // Mock stories for velocity calculation
      const mockStoriesSprint1 = [
        { story_points: 5, status: 'done' },
        { story_points: 8, status: 'done' },
        { story_points: 7, status: 'done' }
      ];

      const mockStoriesSprint2 = [
        { story_points: 10, status: 'done' },
        { story_points: 15, status: 'done' }
      ];

      // Mock database calls
      // First call returns sprints in DESC order (newest first)
      mockSQLiteManager.all
        .mockResolvedValueOnce({ success: true, data: mockSprints })
        // After reversal, sprints are processed as [Sprint 1, Sprint 2]
        .mockResolvedValueOnce({ success: true, data: mockStoriesSprint1 }) // Sprint 1 stories first
        .mockResolvedValueOnce({ success: true, data: mockStoriesSprint2 }); // Sprint 2 stories second

      const response = await request(app)
        .get('/api/agile/velocity')
        .query({ teamName: 'Alpha Team', sprints: '5' });

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toBeDefined();
      expect(response.body.data.labels).toEqual(['Sprint 1', 'Sprint 2']); // Reversed to show oldest first
      expect(response.body.data.completedPoints).toEqual([20, 25]); // Reversed order
      expect(response.body.data.averageVelocity).toBe(23); // (20 + 25) / 2
      expect(response.body.data.currentVelocity).toBe(25); // Last sprint's velocity
    });
  });

  describe('GET /api/agile/burndown/:sprintId', () => {
    it('should return burndown chart data', async () => {
      const mockSprint = {
        id: 'sprint-1',
        name: 'Sprint 1',
        status: 'active',
        start_date: new Date('2024-01-01').getTime() / 1000,
        end_date: new Date('2024-01-14').getTime() / 1000
      };

      const mockStories = [
        { id: 'story-1', story_points: 8, status: 'done', updated_at: new Date('2024-01-05').getTime() / 1000 },
        { id: 'story-2', story_points: 5, status: 'done', updated_at: new Date('2024-01-08').getTime() / 1000 },
        { id: 'story-3', story_points: 10, status: 'in-progress', updated_at: new Date('2024-01-10').getTime() / 1000 },
        { id: 'story-4', story_points: 7, status: 'todo', updated_at: new Date('2024-01-01').getTime() / 1000 }
      ];

      // Mock database calls
      mockSQLiteManager.get.mockResolvedValueOnce({ success: true, data: mockSprint });
      mockSQLiteManager.all.mockResolvedValueOnce({ success: true, data: mockStories });

      const response = await request(app).get('/api/agile/burndown/sprint-1');

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toBeDefined();
      expect(response.body.data.sprintId).toBe('sprint-1');
      expect(response.body.data.sprintName).toBe('Sprint 1');
      expect(response.body.data.totalStoryPoints).toBe(30); // 8 + 5 + 10 + 7
      expect(response.body.data.completedPoints).toBe(13); // 8 + 5
      expect(response.body.data.labels).toBeDefined();
      expect(response.body.data.idealBurndown).toBeDefined();
      expect(response.body.data.actualBurndown).toBeDefined();
    });
  });

  describe('GET /api/agile/boards', () => {
    it('should return all boards', async () => {
      const mockBoards = [
        {
          id: 'board-1',
          name: 'Development Board',
          columns: ['To Do', 'In Progress', 'Done']
        }
      ];

      mockAgileManager.getBoards.mockResolvedValue(mockBoards);

      const response = await request(app).get('/api/agile/boards');

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockBoards);
    });
  });

  describe('POST /api/agile/boards', () => {
    it('should create a new board', async () => {
      const newBoard = {
        name: 'QA Board',
        columns: ['Ready for QA', 'Testing', 'Verified']
      };

      mockAgileManager.createBoard.mockResolvedValue({
        id: 'board-2',
        ...newBoard
      });

      const response = await request(app)
        .post('/api/agile/boards')
        .send(newBoard);

      expect(response.status).toBe(201);
      expect(response.body.success).toBe(true);
      expect(mockAgileManager.createBoard).toHaveBeenCalledWith(newBoard);
    });

    it('should validate required fields', async () => {
      const response = await request(app)
        .post('/api/agile/boards')
        .send({ columns: ['Test'] }); // missing name

      expect(response.status).toBe(400);
      expect(response.body.error).toBe('Board name and columns are required');
    });
  });

  describe('GET /api/agile/backlog', () => {
    it('should return prioritized backlog items', async () => {
      const mockBacklog = [
        {
          id: 'story-10',
          title: 'High priority feature',
          priority: 'high',
          storyPoints: 8
        }
      ];

      mockAgileManager.getBacklogItems.mockResolvedValue(mockBacklog);

      const response = await request(app)
        .get('/api/agile/backlog')
        .query({ includeEstimates: 'true', maxItems: '20' });

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockBacklog);
      expect(mockAgileManager.getBacklogItems).toHaveBeenCalledWith({
        includeEstimates: true,
        maxItems: 20
      });
    });
  });

  describe('POST /api/agile/estimate', () => {
    it('should estimate a story', async () => {
      const estimateData = {
        storyId: 'story-15',
        estimates: { dev1: 5, dev2: 8, dev3: 5 },
        finalEstimate: 5
      };

      mockAgileManager.estimateStory.mockResolvedValue({
        storyId: 'story-15',
        estimate: 5,
        confidence: 85
      });

      const response = await request(app)
        .post('/api/agile/estimate')
        .send(estimateData);

      expect(response.status).toBe(200);
      expect(mockAgileManager.estimateStory).toHaveBeenCalledWith(
        'story-15',
        estimateData
      );
    });
  });

  describe('GET /api/agile/capacity/:sprintId', () => {
    it('should return team capacity for sprint', async () => {
      const mockCapacity = {
        totalHours: 320,
        totalStoryPoints: 40,
        teamMembers: []
      };

      mockAgileManager.getTeamCapacity.mockResolvedValue(mockCapacity);

      const response = await request(app).get('/api/agile/capacity/sprint-1');

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockCapacity);
    });
  });

  describe('GET /api/agile/report/:sprintId', () => {
    it('should generate sprint report', async () => {
      const mockReport = {
        sprint: { id: 'sprint-1', name: 'Sprint 1' },
        metrics: { velocity: 25, completionRate: 85 },
        highlights: ['Completed authentication'],
        issues: ['Technical debt in payment module']
      };

      mockAgileManager.generateSprintReport.mockResolvedValue(mockReport);

      const response = await request(app)
        .get('/api/agile/report/sprint-1')
        .query({ includeMetrics: 'true', format: 'detailed' });

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockReport);
      expect(mockAgileManager.generateSprintReport).toHaveBeenCalledWith('sprint-1', {
        includeMetrics: true,
        includeStories: true,
        includeRetrospective: false,
        format: 'detailed'
      });
    });
  });
});