import { v7 as uuidv7 } from "uuid";
import { CatalogManagerI } from "../src/services/catalog-manager.js";
import {
  AuthStatus,
  ConfigServiceForHub,
  HubService,
} from "../src/services/hub.js";
import { CurrentSetup, SetupManagerI } from "../src/services/setup-manager.js";
import { IdentityServiceI } from "../src/services/identity-service.js";
import {
  UpstreamHandlerOAuthHandler,
  TargetServerChangeNotifier,
} from "../src/services/upstream-handler.js";
import type { TargetServer } from "../src/model/target-servers.js";
import { SecretsStore } from "../src/services/secrets-store.js";
import { EnvVarManager } from "../src/services/env-var-manager.js";
import { MockHubServer } from "./mock-hub-server.js";
import { getMcpxLogger } from "./utils.js";

// Minimal test doubles for dependencies not relevant to Hub connection tests
class StubSetupManager implements SetupManagerI {
  async applySetup() {
    return {
      source: "hub" as const,
      targetServers: {},
      config: {
        toolGroups: [],
        toolExtensions: { services: {} },
        staticOauth: undefined,
        permissions: { default: { block: [] }, consumers: {} },
        auth: { enabled: false },
      },
    };
  }
  isDigesting() {
    return false;
  }
  getCurrentSetup(): CurrentSetup {
    return {
      targetServers: {},
      config: {
        toolGroups: [],
        toolExtensions: { services: {} },
        staticOauth: undefined,
        permissions: { default: { block: [] }, consumers: {} },
        auth: { enabled: false },
        targetServerAttributes: {},
      },
    };
  }
  buildUserConfigChangePayload() {
    return null;
  }
  buildUserTargetServersChangePayload() {
    return null;
  }
}

class StubCatalogManager implements CatalogManagerI {
  setCatalog() {}
  getCatalog() {
    return [];
  }
  getById() {
    return undefined;
  }
  isStrict() {
    return true;
  }
  setAdminStrictnessOverride() {}
  getAdminStrictnessOverride() {
    return false;
  }
  isServerApproved() {
    return true;
  }
  isToolApproved() {
    return true;
  }
  subscribe() {
    return () => {};
  }
}

class StubIdentityService implements IdentityServiceI {
  getIdentity() {
    return {
      mode: "enterprise" as const,
      entity: { entityType: "user" as const, role: "member" as const },
    };
  }
  setIdentity() {}
  isSpace() {
    return false;
  }
  isAdmin() {
    return false;
  }
  hasAdminPrivileges() {
    return false;
  }
  isStrictPermissionsEnabled() {
    return true;
  }
  getDisplayName() {
    return undefined;
  }
}

class StubConfigService implements ConfigServiceForHub {
  registerPostCommitHook() {}
}

class StubTargetClients
  implements TargetServerChangeNotifier, UpstreamHandlerOAuthHandler
{
  registerPostChangeHook(
    _hookName: string,
    _hook: (servers: TargetServer[]) => void,
  ) {}
  async initiateOAuthForServer() {
    return { authorizationUrl: "", state: "", userCode: undefined };
  }
  async completeOAuthByState() {}
}

describe("HubService", () => {
  const HUB_PORT = 9002;
  const HUB_URL = `http://localhost:${HUB_PORT}`;
  const VALID_USER_ID = "valid-user-id-123";
  const INVALID_USER_ID = "invalid-user-id";

  let mockHubServer: MockHubServer;
  let hubService: HubService | null;
  const logger = getMcpxLogger();
  const stubSetupManager = new StubSetupManager();
  const stubCatalogManager = new StubCatalogManager();
  const stubConfigService = new StubConfigService();
  const stubIdentityService = new StubIdentityService();
  const stubTargetClients = new StubTargetClients();
  const stubSecretsStore = new SecretsStore(logger);
  const stubEnvVarManager = new EnvVarManager(logger);
  const stubGetUsageStats = () => ({ agents: [], targetServers: [] });

  beforeEach(async () => {
    // Start mock hub server
    mockHubServer = new MockHubServer({ port: HUB_PORT, logger });

    // Wait for server to start listening
    await mockHubServer.waitForListening();
  });

  afterEach(async () => {
    // Cleanup
    if (hubService) {
      await hubService.disconnect();
      hubService = null;
    }

    await mockHubServer.close();
  });

  describe("Connection with user ID", () => {
    it("should connect successfully with valid user ID", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      const statusPromise = new Promise<AuthStatus>((resolve) => {
        hubService!.addStatusListener((status) => {
          resolve(status);
        });
      });

      const connectResult = await hubService!.connect({
        setupOwnerId: VALID_USER_ID,
      });

      expect(connectResult.status).toBe("authenticated");
      expect(connectResult.connectionError).toBeUndefined();

      // Verify listener was called
      const listenerStatus = await statusPromise;
      expect(listenerStatus.status).toBe("authenticated");

      // Verify server sees the connection
      expect(mockHubServer.getConnectedClients()).toHaveLength(1);
    });

    it("should fail to connect with invalid user ID", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      const connectResult = await hubService.connect({
        setupOwnerId: INVALID_USER_ID,
      });

      expect(connectResult.status).toBe("unauthenticated");
      expect(connectResult.connectionError?.name).toBe("HubUnavailableError");

      // Verify no clients connected
      expect(mockHubServer.getConnectedClients()).toHaveLength(0);
    });

    it("should fail when server is not available", async () => {
      // Close the server to simulate unavailability
      await mockHubServer.close();

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 1000, // Short timeout for faster test
        },
      );

      const connectResult = await hubService.connect({
        setupOwnerId: VALID_USER_ID,
      });

      expect(connectResult.status).toBe("unauthenticated");
      expect(connectResult.connectionError?.name).toBe(
        "HubConnectionTimeoutError",
      );
    });
  });

  describe("Disconnect behavior", () => {
    it("should update status to unauthenticated on disconnect", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      // Connect first
      await hubService.connect({ setupOwnerId: VALID_USER_ID });
      expect(hubService.status.status).toBe("authenticated");

      // Disconnect
      await hubService.disconnect();

      // Verify status is unauthenticated
      expect(hubService.status.status).toBe("unauthenticated");
    });
  });

  describe("Reconnection behavior", () => {
    it("should return existing status when already connected", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      // First connection
      const firstResult = await hubService!.connect({
        setupOwnerId: VALID_USER_ID,
      });
      expect(firstResult.status).toBe("authenticated");

      const firstClients = mockHubServer.getConnectedClients();
      expect(firstClients).toHaveLength(1);
      const firstClientId = firstClients[0];

      // Second connect() call should return existing status without reconnecting
      const secondResult = await hubService!.connect({
        setupOwnerId: VALID_USER_ID,
      });
      expect(secondResult.status).toBe("authenticated");

      // Should still have the same client (no reconnection)
      const secondClients = mockHubServer.getConnectedClients();
      expect(secondClients).toHaveLength(1);
      expect(secondClients[0]).toBe(firstClientId);
    });
  });

  describe("Catalog acknowledgment", () => {
    it("should acknowledge set-catalog message with ok: true", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      await hubService.connect({ setupOwnerId: VALID_USER_ID });
      expect(hubService.status.status).toBe("authenticated");

      const clients = mockHubServer.getConnectedClients();
      expect(clients).toHaveLength(1);
      const socketId = clients[0]!;

      // Emit catalog with ack and verify it's acknowledged
      const ackResult = await mockHubServer.emitCatalogWithAck(socketId, {
        items: [
          {
            server: {
              id: uuidv7(),
              name: "test-server",
              displayName: "Test Server",
              config: { type: "stdio", command: "npx", args: ["-y", "test"] },
            },
          },
        ],
      });

      expect(ackResult).toEqual({ ok: true });
    });

    it("should acknowledge set-catalog with ok: false on parse failure", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      await hubService.connect({ setupOwnerId: VALID_USER_ID });
      const clients = mockHubServer.getConnectedClients();
      const socketId = clients[0]!;

      // Emit invalid catalog (missing required fields)
      const ackResult = await mockHubServer.emitCatalogWithAck(socketId, {
        items: "not-an-array", // Invalid - should be array
      } as unknown as { items: [] });

      expect(ackResult).toEqual({ ok: false });
    });
  });

  describe("Status management", () => {
    it("should notify multiple listeners on status change", async () => {
      mockHubServer.setValidTokens([VALID_USER_ID]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
        },
      );

      const listener1Statuses: AuthStatus[] = [];
      const listener2Statuses: AuthStatus[] = [];

      hubService.addStatusListener((status) => {
        listener1Statuses.push(status);
      });

      hubService.addStatusListener((status) => {
        listener2Statuses.push(status);
      });

      await hubService.connect({ setupOwnerId: VALID_USER_ID });

      // Both listeners should have been notified synchronously
      expect(listener1Statuses).toHaveLength(1);
      expect(listener1Statuses[0]?.status).toBe("authenticated");

      expect(listener2Statuses).toHaveLength(1);
      expect(listener2Statuses[0]?.status).toBe("authenticated");
    });

    it("should not notify when status doesn't change", async () => {
      mockHubServer.setValidTokens([]);

      hubService = new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 1000,
        },
      );

      let notificationCount = 0;
      hubService.addStatusListener(() => {
        notificationCount++;
      });

      // First failed connection
      await hubService.connect({ setupOwnerId: INVALID_USER_ID });
      expect(notificationCount).toBe(1);

      // Second failed connection with same error type
      await hubService.connect({ setupOwnerId: INVALID_USER_ID });

      // Should not notify again since status is the same
      expect(notificationCount).toBe(1);
    });
  });

  describe("Tool call batching", () => {
    const BATCH_INTERVAL_MS = 50;

    function makeHubService(): HubService {
      return new HubService(
        logger,
        stubSetupManager,
        stubCatalogManager,
        stubSecretsStore,
        stubEnvVarManager,
        stubConfigService,
        stubIdentityService,
        stubTargetClients,
        stubGetUsageStats,
        {
          hubUrl: HUB_URL,
          connectionTimeout: 5000,
          toolCallBatchIntervalMs: BATCH_INTERVAL_MS,
        },
      );
    }

    beforeEach(() => {
      mockHubServer.setValidTokens([VALID_USER_ID]);
    });

    it("delivers recorded tool calls to hub on interval flush", async () => {
      hubService = makeHubService();
      await hubService.connect({ setupOwnerId: VALID_USER_ID });

      hubService.recordToolCall({
        serverName: "my-server",
        toolName: "my-tool",
        clientName: "claude-ai",
        consumerTag: "claude",
        durationMs: 123,
        isError: false,
        isCallFailure: false,
      });

      const envelope = await mockHubServer.waitForToolCallBatch(2000);
      const { payload } = envelope as { payload: { events: unknown[] } };

      expect(payload.events).toHaveLength(1);
      expect(payload.events[0]).toMatchObject({
        serverName: "my-server",
        toolName: "my-tool",
        clientName: "claude-ai",
        consumerTag: "claude",
        durationMs: 123,
        errorType: null,
      });
    });

    it("maps isError:true to errorType tool_error", async () => {
      hubService = makeHubService();
      await hubService.connect({ setupOwnerId: VALID_USER_ID });

      hubService.recordToolCall({
        serverName: "s",
        toolName: "t",
        clientName: undefined,
        consumerTag: "c",
        durationMs: 10,
        isError: true,
        isCallFailure: false,
      });

      const envelope = await mockHubServer.waitForToolCallBatch(2000);
      const { payload } = envelope as {
        payload: { events: { errorType: unknown }[] };
      };

      expect(payload.events[0]?.errorType).toBe("tool_error");
    });

    it("maps isCallFailure:true to errorType call_failed", async () => {
      hubService = makeHubService();
      await hubService.connect({ setupOwnerId: VALID_USER_ID });

      hubService.recordToolCall({
        serverName: "s",
        toolName: "t",
        clientName: undefined,
        consumerTag: "c",
        durationMs: 10,
        isError: true,
        isCallFailure: true,
      });

      const envelope = await mockHubServer.waitForToolCallBatch(2000);
      const { payload } = envelope as {
        payload: { events: { errorType: unknown }[] };
      };

      expect(payload.events[0]?.errorType).toBe("call_failed");
    });

    // Shutdown-flush is intentionally not tested here.
    //
    // disconnect() calls batcher.shutdown() which flushes via socket.emit() before
    // calling socket.disconnect(). TCP guarantees the batch packet is transmitted before
    // the disconnect packet, so the hub receives it in production.
    //
    // In-process tests are unreliable for this: socket.io event dispatch goes through
    // microtasks/callbacks, and the server-side handler may not fire before the test
    // assertion runs — even though the data was sent. The BatchBuffer unit test covers
    // the shutdown-flush logic at the pure level.
  });
});
