/**
 * Sequential Device Rotation Tests
 * Verifies SimulatorPool.bootSequential() for low-memory multi-device testing.
 */

import { SimulatorPool } from '../../src/simulator/pool';
import { getSessionManager } from '../../src/session-manager';

// Mock SimulatorManager
jest.mock('../../src/simulator/manager', () => {
  let bootCount = 0;
  return {
    SimulatorManager: jest.fn().mockImplementation(() => ({
      boot: jest.fn().mockImplementation(async (preset: string) => {
        bootCount++;
        return {
          udid: `udid-${preset}-${bootCount}`,
          name: `Mock ${preset}`,
          state: 'Booted',
          isAvailable: true,
          runtime: 'iOS-18-0',
          runtimeVersion: '18.0',
        };
      }),
      shutdown: jest.fn().mockResolvedValue(undefined),
      openUrl: jest.fn().mockResolvedValue(undefined),
      getDevice: jest.fn().mockResolvedValue(null),
    })),
  };
});

// Mock WebKitClient
jest.mock('../../src/webkit/client', () => ({
  WebKitClient: jest.fn().mockImplementation(() => ({
    connect: jest.fn().mockResolvedValue(undefined),
    disconnect: jest.fn().mockResolvedValue(undefined),
    isConnected: () => true,
    setCookies: jest.fn().mockResolvedValue(undefined),
    navigate: jest.fn().mockResolvedValue({ url: '', status: 200, loadTime: 0 }),
  })),
}));

// Mock zombie-cleanup
jest.mock('../../src/reliability/zombie-cleanup', () => ({
  registerManagedDevices: jest.fn(),
  unregisterManagedDevices: jest.fn(),
}));

// Mock os.freemem to always have enough
jest.mock('os', () => ({
  ...jest.requireActual('os'),
  freemem: () => 16 * 1024 * 1024 * 1024, // 16GB
}));

describe('SimulatorPool.bootSequential', () => {
  let pool: SimulatorPool;

  beforeEach(() => {
    pool = new SimulatorPool({ max: 5, concurrency: 3 });
    // Reset SessionManager
    const sm = getSessionManager();
    for (const sim of sm.listSimulators()) {
      sm.removeSimulator(sim.deviceId);
    }
  });

  it('should run each device sequentially and return results', async () => {
    const executionOrder: string[] = [];

    const results = await pool.bootSequential(
      ['iphone-17', 'ipad-pro', 'iphone-se'],
      async (sim, preset) => {
        executionOrder.push(preset);
        return { tested: preset, pages: 5 };
      },
    );

    expect(results.length).toBe(3);
    expect(executionOrder).toEqual(['iphone-17', 'ipad-pro', 'iphone-se']);

    const r1 = results.find(r => r.preset === 'iphone-17');
    expect(r1?.status).toBe('completed');
    expect(r1?.result).toEqual({ tested: 'iphone-17', pages: 5 });

    const r2 = results.find(r => r.preset === 'ipad-pro');
    expect(r2?.status).toBe('completed');
  });

  it('should only have one simulator active at a time', async () => {
    let maxConcurrent = 0;
    let currentActive = 0;

    const results = await pool.bootSequential(
      ['device-a', 'device-b', 'device-c'],
      async () => {
        currentActive++;
        maxConcurrent = Math.max(maxConcurrent, currentActive);
        // Simulate some work
        await new Promise(r => setTimeout(r, 10));
        currentActive--;
        return 'done';
      },
    );

    expect(maxConcurrent).toBe(1);
    expect(results.length).toBe(3);
  });

  it('should continue after a device failure', async () => {
    const results = await pool.bootSequential(
      ['good-1', 'bad-device', 'good-2'],
      async (_sim, preset) => {
        if (preset === 'bad-device') {
          throw new Error('Simulated failure');
        }
        return { ok: true };
      },
    );

    expect(results.length).toBe(3);
    expect(results.find(r => r.preset === 'good-1')?.status).toBe('completed');
    expect(results.find(r => r.preset === 'bad-device')?.status).toBe('failed');
    expect(results.find(r => r.preset === 'bad-device')?.error).toContain('Simulated failure');
    expect(results.find(r => r.preset === 'good-2')?.status).toBe('completed');
  });

  it('should track duration per device', async () => {
    const results = await pool.bootSequential(
      ['fast', 'slow'],
      async (_sim, preset) => {
        if (preset === 'slow') {
          await new Promise(r => setTimeout(r, 50));
        }
        return 'done';
      },
    );

    const fast = results.find(r => r.preset === 'fast')!;
    const slow = results.find(r => r.preset === 'slow')!;
    expect(fast.duration).toBeGreaterThanOrEqual(0);
    expect(slow.duration).toBeGreaterThan(fast.duration);
  });

  it('should shut down device after each run', async () => {
    const results = await pool.bootSequential(
      ['a', 'b'],
      async () => 'done',
    );

    // After sequential completion, pool should be empty
    expect(pool.size).toBe(0);
    expect(results.length).toBe(2);
  });

  it('should handle empty device list', async () => {
    const results = await pool.bootSequential([], async () => 'done');
    expect(results.length).toBe(0);
  });

  it('should handle single device', async () => {
    const results = await pool.bootSequential(
      ['solo-device'],
      async () => ({ result: 42 }),
    );

    expect(results.length).toBe(1);
    expect(results.find(r => r.preset === 'solo-device')?.status).toBe('completed');
    expect(results.find(r => r.preset === 'solo-device')?.result).toEqual({ result: 42 });
  });

  it('should preserve all results for duplicate presets', async () => {
    const results = await pool.bootSequential(
      ['iphone-17', 'iphone-17'],
      async (_sim, preset) => {
        return { tested: preset };
      },
    );

    expect(results.length).toBe(2);
    expect(results[0].preset).toBe('iphone-17');
    expect(results[0].status).toBe('completed');
    expect(results[1].preset).toBe('iphone-17');
    expect(results[1].status).toBe('completed');
  });

  it('should return correct results for mixed duplicate presets', async () => {
    let callCount = 0;
    const results = await pool.bootSequential(
      ['a', 'b', 'a'],
      async (_sim, preset) => {
        callCount++;
        return { preset, call: callCount };
      },
    );

    expect(results.length).toBe(3);
    expect(results[0]).toMatchObject({ preset: 'a', status: 'completed' });
    expect(results[0].result).toEqual({ preset: 'a', call: 1 });
    expect(results[1]).toMatchObject({ preset: 'b', status: 'completed' });
    expect(results[1].result).toEqual({ preset: 'b', call: 2 });
    expect(results[2]).toMatchObject({ preset: 'a', status: 'completed' });
    expect(results[2].result).toEqual({ preset: 'a', call: 3 });
    expect(results.every(r => r.duration >= 0)).toBe(true);
  });

  it('should pass correct index to runner callback', async () => {
    const receivedIndices: number[] = [];
    await pool.bootSequential(
      ['x', 'y', 'z'],
      async (_sim, _preset, index) => {
        receivedIndices.push(index);
        return 'done';
      },
    );

    expect(receivedIndices).toEqual([0, 1, 2]);
  });
});

// Mock AuthManager
jest.mock('../../src/auth/manager', () => ({
  AuthManager: jest.fn().mockImplementation(() => ({
    loadProfile: jest.fn().mockResolvedValue({ cookies: [], localStorage: {}, sessionStorage: {} }),
  })),
}));

// Mock WorkflowPersistence
jest.mock('../../src/orchestration/workflow-persistence', () => ({
  WorkflowPersistence: jest.fn().mockImplementation(() => ({
    save: jest.fn(),
    loadAll: jest.fn().mockReturnValue([]),
    remove: jest.fn(),
  })),
}));

import { SimulatorWorkflowEngine } from '../../src/orchestration/workflow-engine';
import { AuthManager } from '../../src/auth/manager';

describe('WorkflowEngine sequential mode', () => {
  it('should accept mode parameter in WorkflowInitOptions', () => {
    // Type-level test: ensure mode is accepted
    const options = {
      devices: ['iphone-17'],
      mode: 'sequential' as const,
    };
    expect(options.mode).toBe('sequential');
  });

  it('should default to concurrent mode', () => {
    const options: { devices: string[]; mode?: string } = { devices: ['iphone-17'] };
    expect(options.mode ?? 'concurrent').toBe('concurrent');
  });

  it('should generate completion prompts for sequential mode', async () => {
    // Create a mocked pool with bootSequential returning a Map of results
    const mockPool = {
      bootSequential: jest.fn().mockImplementation(
        async (presets: string[], _runner: unknown) => {
          const results = new Map<string, { status: string; result?: unknown; error?: string; duration: number }>();
          for (const preset of presets) {
            results.set(preset, {
              status: 'completed',
              result: { tested: preset },
              duration: 100,
            });
          }
          return results;
        },
      ),
      bootAll: jest.fn(),
      shutdownAll: jest.fn(),
      injectAuth: jest.fn(),
    } as unknown as SimulatorPool;

    const mockAuth = new AuthManager();
    const engine = new SimulatorWorkflowEngine(mockPool, mockAuth);

    const result = await engine.initWorkflow({
      devices: ['iphone-17', 'ipad-pro'],
      mode: 'sequential',
      taskDescription: 'Test login flow',
    });

    // Verify prompts contain completion markers
    expect(result.prompts).toHaveLength(2);
    for (const p of result.prompts) {
      expect(p.prompt).toContain('[COMPLETED]');
      expect(p.prompt).toContain('Results available via workflow_collect');
      expect(p.prompt).not.toContain('Use these tools');
    }
  });

  it('should include worker name and preset in sequential completion prompts', async () => {
    const mockPool = {
      bootSequential: jest.fn().mockImplementation(
        async (presets: string[], _runner: unknown) => {
          return presets.map(preset => ({
            preset,
            status: 'completed' as const,
            result: { tested: preset },
            duration: 50,
          }));
        },
      ),
      bootAll: jest.fn(),
      shutdownAll: jest.fn(),
      injectAuth: jest.fn(),
    } as unknown as SimulatorPool;

    const mockAuth = new AuthManager();
    const engine = new SimulatorWorkflowEngine(mockPool, mockAuth);

    const result = await engine.initWorkflow({
      devices: ['iphone-se'],
      workerNames: ['mobile-tester'],
      mode: 'sequential',
    });

    expect(result.prompts).toHaveLength(1);
    expect(result.prompts[0].workerName).toBe('mobile-tester');
    expect(result.prompts[0].prompt).toContain('mobile-tester');
    expect(result.prompts[0].prompt).toContain('iphone-se');
    expect(result.prompts[0].prompt).toContain('completed');
  });

  it('should show failed status in sequential prompts when device fails', async () => {
    const mockPool = {
      bootSequential: jest.fn().mockImplementation(
        async (presets: string[], _runner: unknown) => {
          return presets.map(preset => {
            if (preset === 'bad-device') {
              return { preset, status: 'failed' as const, error: 'Boot failed', duration: 10 };
            }
            return { preset, status: 'completed' as const, result: { ok: true }, duration: 100 };
          });
        },
      ),
      bootAll: jest.fn(),
      shutdownAll: jest.fn(),
      injectAuth: jest.fn(),
    } as unknown as SimulatorPool;

    const mockAuth = new AuthManager();
    const engine = new SimulatorWorkflowEngine(mockPool, mockAuth);

    const result = await engine.initWorkflow({
      devices: ['iphone-17', 'bad-device'],
      mode: 'sequential',
    });

    expect(result.prompts).toHaveLength(2);

    const goodPrompt = result.prompts.find(p => p.workerName === 'worker-iphone-17')!;
    expect(goodPrompt.prompt).toContain('[COMPLETED]');
    expect(goodPrompt.prompt).toContain('completed');

    const badPrompt = result.prompts.find(p => p.workerName === 'worker-bad-device')!;
    expect(badPrompt.prompt).toContain('[COMPLETED]');
    expect(badPrompt.prompt).toContain('failed');
  });
});
