import { beforeEach, describe, expect, it, vi } from 'vitest';

import { ProjectScanner } from '../../../src/main/services/discovery/ProjectScanner';

import type { Project, SearchSessionsResult } from '../../../src/main/types';

/**
 * Tests for global search functionality across all projects
 */
describe('Global Search - ProjectScanner.searchAllProjects', () => {
  let projectScanner: ProjectScanner;
  let mockScan: ReturnType<typeof vi.fn>;
  let mockSearchSessions: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    // Create a real ProjectScanner instance
    projectScanner = new ProjectScanner();

    // Mock the scan() method
    mockScan = vi.fn();
    projectScanner.scan = mockScan;

    // Mock the sessionSearcher.searchSessions() method
    mockSearchSessions = vi.fn();
    // @ts-expect-error - Accessing private property for testing
    projectScanner.sessionSearcher = {
      searchSessions: mockSearchSessions,
    };
  });

  describe('searchAllProjects', () => {
    it('should return empty results for empty query', async () => {
      const result = await projectScanner.searchAllProjects('', 50);

      expect(result.results).toEqual([]);
      expect(result.totalMatches).toBe(0);
      expect(result.sessionsSearched).toBe(0);
      expect(mockScan).not.toHaveBeenCalled();
    });

    it('should return empty results for whitespace query', async () => {
      const result = await projectScanner.searchAllProjects('   ', 50);

      expect(result.results).toEqual([]);
      expect(result.totalMatches).toBe(0);
      expect(result.sessionsSearched).toBe(0);
      expect(mockScan).not.toHaveBeenCalled();
    });

    it('should return empty results when no projects exist', async () => {
      mockScan.mockResolvedValue([]);

      const result = await projectScanner.searchAllProjects('test', 50);

      expect(result.results).toEqual([]);
      expect(result.totalMatches).toBe(0);
      expect(result.sessionsSearched).toBe(0);
      expect(mockScan).toHaveBeenCalledOnce();
    });

    it('should search across multiple projects and merge results', async () => {
      const now = Date.now();

      // Mock scan() to return 2 projects
      const mockProjects: Project[] = [
        {
          id: 'project1',
          path: '/path/to/project1',
          name: 'Project 1',
          sessions: ['session1'],
          createdAt: now - 10000,
          mostRecentSession: now,
        },
        {
          id: 'project2',
          path: '/path/to/project2',
          name: 'Project 2',
          sessions: ['session2'],
          createdAt: now - 20000,
          mostRecentSession: now - 1000,
        },
      ];
      mockScan.mockResolvedValue(mockProjects);

      // Mock searchSessions() to return different results for each project
      mockSearchSessions.mockImplementation((projectId: string) => {
        if (projectId === 'project1') {
          return Promise.resolve({
            results: [
              {
                projectId: 'project1',
                sessionId: 'session1',
                sessionTitle: 'Test Session 1',
                context: 'This is a test message',
                matchedText: 'test',
                messageType: 'user' as const,
                timestamp: now,
                groupId: 'group1',
                matchIndexInItem: 0,
                matchStartOffset: 10,
                messageUuid: 'uuid1',
              },
            ],
            totalMatches: 1,
            sessionsSearched: 5,
            query: 'test',
          } satisfies SearchSessionsResult);
        } else {
          return Promise.resolve({
            results: [
              {
                projectId: 'project2',
                sessionId: 'session2',
                sessionTitle: 'Test Session 2',
                context: 'Another test message',
                matchedText: 'test',
                messageType: 'assistant' as const,
                timestamp: now - 1000,
                groupId: 'group2',
                matchIndexInItem: 0,
                matchStartOffset: 8,
                messageUuid: 'uuid2',
              },
            ],
            totalMatches: 1,
            sessionsSearched: 3,
            query: 'test',
          } satisfies SearchSessionsResult);
        }
      });

      const result = await projectScanner.searchAllProjects('test', 50);

      expect(mockScan).toHaveBeenCalledOnce();
      expect(mockSearchSessions).toHaveBeenCalledTimes(2);
      expect(mockSearchSessions).toHaveBeenCalledWith('project1', 'test', 50);
      expect(mockSearchSessions).toHaveBeenCalledWith('project2', 'test', 50);

      expect(result.results).toHaveLength(2);
      expect(result.totalMatches).toBe(2);
      expect(result.sessionsSearched).toBe(8); // 5 + 3

      // Verify results from different projects
      expect(result.results[0].projectId).toBe('project1');
      expect(result.results[1].projectId).toBe('project2');
    });

    it('should sort results by timestamp (most recent first)', async () => {
      const now = Date.now();

      const mockProjects: Project[] = [
        {
          id: 'project1',
          path: '/path/to/project1',
          name: 'Project 1',
          sessions: ['session1'],
          createdAt: now - 10000,
        },
        {
          id: 'project2',
          path: '/path/to/project2',
          name: 'Project 2',
          sessions: ['session2'],
          createdAt: now - 20000,
        },
      ];
      mockScan.mockResolvedValue(mockProjects);

      // Project1 has older result, Project2 has newer result
      mockSearchSessions.mockImplementation((projectId: string) => {
        if (projectId === 'project1') {
          return Promise.resolve({
            results: [
              {
                projectId: 'project1',
                sessionId: 'session1',
                sessionTitle: 'Old Session',
                context: 'test',
                matchedText: 'test',
                messageType: 'user' as const,
                timestamp: now - 10000, // Older
                groupId: 'group1',
                matchIndexInItem: 0,
                matchStartOffset: 0,
                messageUuid: 'uuid1',
              },
            ],
            totalMatches: 1,
            sessionsSearched: 5,
            query: 'test',
          } satisfies SearchSessionsResult);
        } else {
          return Promise.resolve({
            results: [
              {
                projectId: 'project2',
                sessionId: 'session2',
                sessionTitle: 'New Session',
                context: 'test',
                matchedText: 'test',
                messageType: 'user' as const,
                timestamp: now, // Newer
                groupId: 'group2',
                matchIndexInItem: 0,
                matchStartOffset: 0,
                messageUuid: 'uuid2',
              },
            ],
            totalMatches: 1,
            sessionsSearched: 3,
            query: 'test',
          } satisfies SearchSessionsResult);
        }
      });

      const result = await projectScanner.searchAllProjects('test', 50);

      // Should be sorted newest first
      expect(result.results[0].sessionTitle).toBe('New Session');
      expect(result.results[1].sessionTitle).toBe('Old Session');
      expect(result.results[0].timestamp).toBeGreaterThan(result.results[1].timestamp);
    });

    it('should respect maxResults limit', async () => {
      const now = Date.now();

      const mockProjects: Project[] = [
        {
          id: 'project1',
          path: '/path/to/project1',
          name: 'Project 1',
          sessions: ['session1'],
          createdAt: now,
        },
      ];
      mockScan.mockResolvedValue(mockProjects);

      // Return 30 results from search
      const mockResults = Array.from({ length: 30 }, (_, i) => ({
        projectId: 'project1',
        sessionId: `session${i}`,
        sessionTitle: `Session ${i}`,
        context: 'test context',
        matchedText: 'test',
        messageType: 'user' as const,
        timestamp: now - i * 1000,
        groupId: `group${i}`,
        matchIndexInItem: 0,
        matchStartOffset: 0,
        messageUuid: `uuid${i}`,
      }));

      mockSearchSessions.mockResolvedValue({
        results: mockResults,
        totalMatches: 30,
        sessionsSearched: 50,
        query: 'test',
      } satisfies SearchSessionsResult);

      const result = await projectScanner.searchAllProjects('test', 25);

      expect(result.results.length).toBe(25); // Limited to maxResults
      expect(mockSearchSessions).toHaveBeenCalledWith('project1', 'test', 25);
    });

    it('should handle search errors gracefully', async () => {
      const now = Date.now();

      const mockProjects: Project[] = [
        {
          id: 'project1',
          path: '/path/to/project1',
          name: 'Project 1',
          sessions: ['session1'],
          createdAt: now,
        },
        {
          id: 'project2',
          path: '/path/to/project2',
          name: 'Project 2',
          sessions: ['session2'],
          createdAt: now - 1000,
        },
      ];
      mockScan.mockResolvedValue(mockProjects);

      // First project fails, second succeeds
      mockSearchSessions.mockImplementation((projectId: string) => {
        if (projectId === 'project1') {
          return Promise.reject(new Error('Search failed'));
        } else {
          return Promise.resolve({
            results: [
              {
                projectId: 'project2',
                sessionId: 'session2',
                sessionTitle: 'Test Session 2',
                context: 'test',
                matchedText: 'test',
                messageType: 'user' as const,
                timestamp: now,
                groupId: 'group2',
                matchIndexInItem: 0,
                matchStartOffset: 0,
                messageUuid: 'uuid2',
              },
            ],
            totalMatches: 1,
            sessionsSearched: 3,
            query: 'test',
          } satisfies SearchSessionsResult);
        }
      });

      const result = await projectScanner.searchAllProjects('test', 50);

      // Should still return results from successful project
      expect(result.results).toHaveLength(1);
      expect(result.results[0].projectId).toBe('project2');
      expect(result.totalMatches).toBe(1);
      expect(result.sessionsSearched).toBe(3);
    });

    it('should use batched concurrency for local FS', async () => {
      const now = Date.now();

      // Create 10 projects to test batching (local uses batch size 4)
      const mockProjects: Project[] = Array.from({ length: 10 }, (_, i) => ({
        id: `project${i}`,
        path: `/path/to/project${i}`,
        name: `Project ${i}`,
        sessions: [`session${i}`],
        createdAt: now - i * 1000,
      }));
      mockScan.mockResolvedValue(mockProjects);

      // Track call order to verify batching
      const callOrder: string[] = [];
      mockSearchSessions.mockImplementation((projectId: string) => {
        callOrder.push(projectId);
        return Promise.resolve({
          results: [],
          totalMatches: 0,
          sessionsSearched: 1,
          query: 'test',
        } satisfies SearchSessionsResult);
      });

      await projectScanner.searchAllProjects('test', 50);

      // All 10 projects should be searched
      expect(mockSearchSessions).toHaveBeenCalledTimes(10);
      expect(callOrder).toHaveLength(10);
    });

    it('should stop searching when enough results are found', async () => {
      const now = Date.now();

      // Create 10 projects
      const mockProjects: Project[] = Array.from({ length: 10 }, (_, i) => ({
        id: `project${i}`,
        path: `/path/to/project${i}`,
        name: `Project ${i}`,
        sessions: [`session${i}`],
        createdAt: now - i * 1000,
      }));
      mockScan.mockResolvedValue(mockProjects);

      // Each project returns 10 results (total would be 100)
      mockSearchSessions.mockImplementation((projectId: string) => {
        const results = Array.from({ length: 10 }, (_, i) => ({
          projectId,
          sessionId: `session${i}`,
          sessionTitle: `Session ${i}`,
          context: 'test',
          matchedText: 'test',
          messageType: 'user' as const,
          timestamp: now - i * 1000,
          groupId: `group${i}`,
          matchIndexInItem: 0,
          matchStartOffset: 0,
          messageUuid: `uuid${i}`,
        }));

        return Promise.resolve({
          results,
          totalMatches: 10,
          sessionsSearched: 1,
          query: 'test',
        } satisfies SearchSessionsResult);
      });

      const result = await projectScanner.searchAllProjects('test', 50);

      // Should stop after getting enough results (checks after each batch of 4)
      // Batch 1 (4 projects): 40 matches < 50, continue
      // Batch 2 (4 projects): 80 matches >= 50, stop
      expect(mockSearchSessions.mock.calls.length).toBeGreaterThanOrEqual(4);
      expect(mockSearchSessions.mock.calls.length).toBeLessThanOrEqual(8);

      // Result should be limited to maxResults
      expect(result.results.length).toBe(50);
    });
  });
});
