import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { IOSCtrlProxyClient } from "../../../../src/features/observe/ios";
import { BootedDevice } from "../../../../src/models";
import {
  createInstantFailureWebSocketFactory,
  createSuccessWebSocketFactory,
} from "../../../fakes/FakeWebSocket";
import { FakeTimer } from "../../../fakes/FakeTimer";
import { FakeIOSCtrlProxyManager } from "../../../fakes/FakeIOSCtrlProxyManager";
import type { ServiceManagerFactory, BootedDeviceLister } from "../../../../src/features/observe/ios/IOSCtrlProxyClient";

describe("IOSCtrlProxyClient auto-setup", function() {
  let testDevice: BootedDevice;
  let fakeTimer: FakeTimer;
  let fakeManager: FakeIOSCtrlProxyManager;
  const serverPort = 8765;

  beforeEach(function() {
    fakeTimer = new FakeTimer();
    fakeTimer.enableAutoAdvance();

    testDevice = {
      deviceId: "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
      platform: "ios",
      name: "iPhone 16 Simulator",
    };

    fakeManager = new FakeIOSCtrlProxyManager();

    IOSCtrlProxyClient.resetInstances();
  });

  afterEach(async function() {
    IOSCtrlProxyClient.resetInstances();
  });

  const createManagerFactory = (): ServiceManagerFactory => {
    return () => fakeManager;
  };

  test("auto-setup triggered when WebSocket fails", async function() {
    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory()
    );

    await client.ensureConnected();

    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(true);

    await client.close();
  });

  test("connect succeeds after auto-setup starts service", async function() {
    let callCount = 0;
    const wsFactory = (url: string) => {
      callCount++;
      if (callCount <= 1) {
        // First call fails (before auto-setup)
        return createInstantFailureWebSocketFactory(fakeTimer)(url);
      }
      // After auto-setup, connection succeeds
      return createSuccessWebSocketFactory(fakeTimer)(url);
    };

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      wsFactory,
      fakeTimer,
      createManagerFactory()
    );

    const result = await client.ensureConnected();

    expect(result).toBe(true);
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(true);

    await client.close();
  });

  test("no auto-setup when already connected", async function() {
    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createSuccessWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory()
    );

    const result = await client.ensureConnected();

    expect(result).toBe(true);
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(false);

    await client.close();
  });

  test("setup failure handled gracefully", async function() {
    fakeManager.setSetupShouldFail(true);

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory()
    );

    const result = await client.ensureConnected();

    expect(result).toBe(false);
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(true);

    await client.close();
  });

  test("guard prevents re-entry during auto-setup", async function() {
    // Create a manager where setup triggers another ensureConnected call
    let reentrantCallResult: boolean | null = null;
    // Use a ref object so the closure captures a mutable reference
    const clientRef: { current: IOSCtrlProxyClient | null } = { current: null };

    const reentrantManager = new FakeIOSCtrlProxyManager();
    const originalSetup = reentrantManager.setup.bind(reentrantManager);
    reentrantManager.setup = async (force, perf) => {
      // During setup, try calling ensureConnected again (simulates re-entry)
      reentrantCallResult = await clientRef.current!.ensureConnected();
      return originalSetup(force, perf);
    };

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      () => reentrantManager
    );
    clientRef.current = client;

    await client.ensureConnected();

    // The re-entrant call should have returned false immediately
    expect(reentrantCallResult).toBe(false);

    await client.close();
  });

  test("skips auto-setup when target simulator is no longer booted", async function() {
    // Device lister returns empty — simulator has been shut down
    const lister: BootedDeviceLister = async () => [];

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory(),
      lister
    );

    const result = await client.ensureConnected();

    expect(result).toBe(false);
    // setup should NOT have been called since the simulator is not booted
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(false);

    await client.close();
  });

  test("skips auto-setup when a different simulator is booted", async function() {
    // A different simulator is booted, but not our target
    const otherDevice: BootedDevice = {
      deviceId: "FFFFFFFF-0000-1111-2222-333333333333",
      platform: "ios",
      name: "iPhone 15 Simulator",
    };
    const lister: BootedDeviceLister = async () => [otherDevice];

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory(),
      lister
    );

    const result = await client.ensureConnected();

    expect(result).toBe(false);
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(false);

    await client.close();
  });

  test("proceeds with auto-setup when target simulator is still booted", async function() {
    // Device lister returns our target simulator as booted
    const lister: BootedDeviceLister = async () => [testDevice];

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory(),
      lister
    );

    await client.ensureConnected();

    // setup should have been called since the simulator is booted
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(true);

    await client.close();
  });

  test("proceeds with auto-setup when boot check fails", async function() {
    // Device lister throws — should not prevent auto-setup
    const lister: BootedDeviceLister = async () => {
      throw new Error("simctl not available");
    };

    const client = IOSCtrlProxyClient.createForTesting(
      testDevice,
      serverPort,
      createInstantFailureWebSocketFactory(fakeTimer),
      fakeTimer,
      createManagerFactory(),
      lister
    );

    await client.ensureConnected();

    // setup should still proceed when boot check fails
    expect(fakeManager.wasMethodCalled("setup:force=true")).toBe(true);

    await client.close();
  });
});
