import { beforeEach, describe, expect, it, vi } from "vitest";
import type { TwitchAccountConfig } from "./types.js";
import { probeTwitch } from "./probe.js";

// Mock Twurple modules - Vitest v4 compatible mocking
const mockUnbind = vi.fn();

// Event handler storage
let connectHandler: (() => void) | null = null;
let disconnectHandler: ((manually: boolean, reason?: Error) => void) | null = null;

// Event listener mocks that store handlers and return unbind function
const mockOnConnect = vi.fn((handler: () => void) => {
  connectHandler = handler;
  return { unbind: mockUnbind };
});

const mockOnDisconnect = vi.fn((handler: (manually: boolean, reason?: Error) => void) => {
  disconnectHandler = handler;
  return { unbind: mockUnbind };
});

const mockOnAuthenticationFailure = vi.fn((_handler: () => void) => {
  return { unbind: mockUnbind };
});

// Connect mock that triggers the registered handler
const defaultConnectImpl = async () => {
  // Simulate successful connection by calling the handler after a delay
  if (connectHandler) {
    await new Promise((resolve) => setTimeout(resolve, 1));
    connectHandler();
  }
};

const mockConnect = vi.fn().mockImplementation(defaultConnectImpl);

const mockQuit = vi.fn().mockResolvedValue(undefined);

vi.mock("@twurple/chat", () => ({
  ChatClient: class {
    connect = mockConnect;
    quit = mockQuit;
    onConnect = mockOnConnect;
    onDisconnect = mockOnDisconnect;
    onAuthenticationFailure = mockOnAuthenticationFailure;
  },
}));

vi.mock("@twurple/auth", () => ({
  StaticAuthProvider: class {},
}));

describe("probeTwitch", () => {
  const mockAccount: TwitchAccountConfig = {
    username: "testbot",
    token: "oauth:test123456789",
    channel: "testchannel",
  };

  beforeEach(() => {
    vi.clearAllMocks();
    // Reset handlers
    connectHandler = null;
    disconnectHandler = null;
  });

  it("returns error when username is missing", async () => {
    const account = { ...mockAccount, username: "" };
    const result = await probeTwitch(account, 5000);

    expect(result.ok).toBe(false);
    expect(result.error).toContain("missing credentials");
  });

  it("returns error when token is missing", async () => {
    const account = { ...mockAccount, token: "" };
    const result = await probeTwitch(account, 5000);

    expect(result.ok).toBe(false);
    expect(result.error).toContain("missing credentials");
  });

  it("attempts connection regardless of token prefix", async () => {
    // Note: probeTwitch doesn't validate token format - it tries to connect with whatever token is provided
    // The actual connection would fail in production with an invalid token
    const account = { ...mockAccount, token: "raw_token_no_prefix" };
    const result = await probeTwitch(account, 5000);

    // With mock, connection succeeds even without oauth: prefix
    expect(result.ok).toBe(true);
  });

  it("successfully connects with valid credentials", async () => {
    const result = await probeTwitch(mockAccount, 5000);

    expect(result.ok).toBe(true);
    expect(result.connected).toBe(true);
    expect(result.username).toBe("testbot");
    expect(result.channel).toBe("testchannel"); // uses account's configured channel
  });

  it("uses custom channel when specified", async () => {
    const account: TwitchAccountConfig = {
      ...mockAccount,
      channel: "customchannel",
    };

    const result = await probeTwitch(account, 5000);

    expect(result.ok).toBe(true);
    expect(result.channel).toBe("customchannel");
  });

  it("times out when connection takes too long", async () => {
    mockConnect.mockImplementationOnce(() => new Promise(() => {})); // Never resolves

    const result = await probeTwitch(mockAccount, 100);

    expect(result.ok).toBe(false);
    expect(result.error).toContain("timeout");

    // Reset mock
    mockConnect.mockImplementation(defaultConnectImpl);
  });

  it("cleans up client even on failure", async () => {
    mockConnect.mockImplementationOnce(async () => {
      // Simulate connection failure by calling disconnect handler
      // onDisconnect signature: (manually: boolean, reason?: Error) => void
      if (disconnectHandler) {
        await new Promise((resolve) => setTimeout(resolve, 1));
        disconnectHandler(false, new Error("Connection failed"));
      }
    });

    const result = await probeTwitch(mockAccount, 5000);

    expect(result.ok).toBe(false);
    expect(result.error).toContain("Connection failed");
    expect(mockQuit).toHaveBeenCalled();

    // Reset mocks
    mockConnect.mockImplementation(defaultConnectImpl);
  });

  it("handles connection errors gracefully", async () => {
    mockConnect.mockImplementationOnce(async () => {
      // Simulate connection failure by calling disconnect handler
      // onDisconnect signature: (manually: boolean, reason?: Error) => void
      if (disconnectHandler) {
        await new Promise((resolve) => setTimeout(resolve, 1));
        disconnectHandler(false, new Error("Network error"));
      }
    });

    const result = await probeTwitch(mockAccount, 5000);

    expect(result.ok).toBe(false);
    expect(result.error).toContain("Network error");

    // Reset mock
    mockConnect.mockImplementation(defaultConnectImpl);
  });

  it("trims token before validation", async () => {
    const account: TwitchAccountConfig = {
      ...mockAccount,
      token: "  oauth:test123456789  ",
    };

    const result = await probeTwitch(account, 5000);

    expect(result.ok).toBe(true);
  });

  it("handles non-Error objects in catch block", async () => {
    mockConnect.mockImplementationOnce(async () => {
      // Simulate connection failure by calling disconnect handler
      // onDisconnect signature: (manually: boolean, reason?: Error) => void
      if (disconnectHandler) {
        await new Promise((resolve) => setTimeout(resolve, 1));
        disconnectHandler(false, "String error" as unknown as Error);
      }
    });

    const result = await probeTwitch(mockAccount, 5000);

    expect(result.ok).toBe(false);
    expect(result.error).toBe("String error");

    // Reset mock
    mockConnect.mockImplementation(defaultConnectImpl);
  });
});
