import { Logger } from "winston";
import { LaplaceConfiguration } from "../utilities/configuration";
import "./client_test_suite";
import { BISTBidAskStreamData, BISTStockStreamData, LivePriceClient, OrderbookLiveData } from "../client/live-price";
import { LivePriceFeed } from "../client/live-price-web-socket";

describe("LivePrice", () => {
  let client: LivePriceClient;
  let config: LaplaceConfiguration;
  let logger: Logger;
  let activeConnections: any[] = [];
  let activeTimeouts: NodeJS.Timeout[] = [];

  const TEST_CONSTANTS = {
    JEST_TIMEOUT: 15000,
    MAIN_TIMEOUT: 10000,
  };

  beforeAll(async () => {
    config = (global as any).testSuite.config as LaplaceConfiguration;
    logger = {
      info: jest.fn(),
      error: jest.fn(),
      warn: jest.fn(),
      debug: jest.fn(),
    } as unknown as Logger;

    client = new LivePriceClient(config, logger);
  });

  afterEach(async () => {
    // Clear all active timeouts
    for (const timeout of activeTimeouts) {
      clearTimeout(timeout);
    }
    activeTimeouts = [];

    // Clean up all active connections
    for (const connection of activeConnections) {
      try {
        connection.close();
      } catch (error) {
        console.log("Error closing connection:", error);
      }
    }
    activeConnections = [];
  });

  afterAll(async () => {
    // Final cleanup
    for (const timeout of activeTimeouts) {
      clearTimeout(timeout);
    }
    for (const connection of activeConnections) {
      try {
        connection.close();
      } catch (error) {
        console.log("Error closing connection in afterAll:", error);
      }
    }
  });

  describe("GetLivePriceForBIST", () => {
    it(
      "should receive BIST live price data",
      async () => {
        const symbols = ["AKBNK"];
        let receivedData: BISTStockStreamData | null = null;
        let receivedError: Error | null = null;

        const lc = client.getLivePriceForBIST(symbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          // Set a timeout to avoid hanging
          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout waiting for data")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData = data;
                break; // Get first data and exit
              }
            } catch (error) {
              console.log("Error in data stream:", error);
            }
          })();

          await Promise.race([dataPromise, timeoutPromise]);

          if (receivedData) {
            const tempReceivedData = (receivedData as BISTStockStreamData).d;
            console.log("Received BIST data:", tempReceivedData);
            expect(tempReceivedData.s).toBeDefined();
            expect(typeof tempReceivedData.s).toBe("string");
            expect(typeof tempReceivedData.p).toBe("number");
            expect(typeof tempReceivedData.ch).toBe("number");
            expect(typeof tempReceivedData.d).toBe("number");
          } else {
            console.log("Timeout waiting for BIST data");
          }
        } catch (error) {
          receivedError = error as Error;
          console.log("Received error:", receivedError.message);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("GetLivePriceForUS", () => {
    it(
      "should receive US live price data",
      async () => {
        const symbols = ["AAPL"];
        let receivedData: any = null;
        let receivedError: Error | null = null;

        const lc = client.getLivePriceForUS(symbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          // Set a timeout to avoid hanging
          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout waiting for data")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData = data;
                break; // Get first data and exit
              }
            } catch (error) {
              console.log("Error in data stream:", error);
            }
          })();

          await Promise.race([dataPromise, timeoutPromise]);

          if (receivedData) {
            console.log("Received US data:", receivedData);
            expect(receivedData.s).toBeDefined();
            expect(typeof receivedData.s).toBe("string");
            expect(typeof receivedData.p).toBe("number");
            expect(typeof receivedData.d).toBe("number");
            expect(typeof receivedData.pc).toBe("number");
            expect(typeof receivedData.ac).toBe("number");
          } else {
            console.log("Timeout waiting for US data");
          }
        } catch (error) {
          receivedError = error as Error;
          console.log("Received error:", receivedError.message);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("LivePriceSubscribe", () => {
    it(
      "should handle subscription changes",
      async () => {
        const initialSymbols = ["AKBNK"];
        const newSymbols = ["TUPRS", "ASELS"];
        const receivedData: string[] = [];
        let switchOccurred = false;

        const lc = client.getLivePriceForBIST(initialSymbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          // Start receiving data
          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData.push(data.d.s);

                // Switch symbols after 5 seconds
                if (!switchOccurred && receivedData.length > 0) {
                  const switchTimeout = setTimeout(async () => {
                    try {
                      await lc.subscribe(newSymbols);
                      receivedData.push("SWITCH");
                      switchOccurred = true;

                      // Close after another 5 seconds
                      const closeTimeout = setTimeout(() => {
                        lc.close();
                      }, 5000);
                      activeTimeouts.push(closeTimeout);
                    } catch (error) {
                      console.error("Error switching symbols:", error);
                    }
                  }, 5000);
                  activeTimeouts.push(switchTimeout);
                }
              }
            } catch (error) {
              console.log("Error in subscription test:", error);
            }
          })();

          // Set overall timeout
          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Test timeout")),
              TEST_CONSTANTS.JEST_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          await Promise.race([dataPromise, timeoutPromise]);

          // Verify we received data
          expect(receivedData.length).toBeGreaterThan(0);

          const switchIndex = receivedData.indexOf("SWITCH");
          if (switchIndex > 0) {
            const beforeSwitch = receivedData.slice(0, switchIndex);
            expect(beforeSwitch.some((symbol) => symbol === "AKBNK")).toBe(
              true
            );
          }

          if (switchIndex >= 0 && switchIndex < receivedData.length - 1) {
            const afterSwitch = receivedData.slice(switchIndex + 1);
            expect(afterSwitch.some((symbol) => symbol === "TUPRS")).toBe(true);
            expect(afterSwitch.some((symbol) => symbol === "ASELS")).toBe(true);
          }
        } catch (error) {
          console.log("Test error:", error);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("LivePriceClose", () => {
    it(
      "should close connection properly",
      async () => {
        const symbols = ["AKBNK"];
        const lc = client.getLivePriceForBIST(symbols);
        activeConnections.push(lc);

        try {
          // Close immediately
          lc.close();

          // Try to receive data after close
          const receiveChan = lc.receive();
          let receivedAfterClose = false;

          try {
            for await (const data of receiveChan) {
              receivedAfterClose = true;
              break;
            }
          } catch (error) {
            // Expected to throw after close
            console.log("Expected error after close:", error);
          }

          // Should not receive data after close
          expect(receivedAfterClose).toBe(false);
        } catch (error) {
          console.error("Close test error:", error);
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("GetDelayedPriceForBIST", () => {
    it(
      "should receive BIST delayed price data",
      async () => {
        const symbols = ["AKBNK"];
        let receivedData: BISTStockStreamData | null = null; 
        let receivedError: Error | null = null;

        const lc = client.getDelayedPriceForBIST(symbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout waiting for delayed data")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData = data;
                break;
              }
            } catch (error) {
              console.log("Error in delayed data stream:", error);
            }
          })();

          await Promise.race([dataPromise, timeoutPromise]);

          if (receivedData != null) {
            const tempReceivedData = (receivedData as BISTStockStreamData).d;
            console.log("Received BIST delayed data:", tempReceivedData);
            expect(tempReceivedData.s).toBeDefined();
            expect(typeof tempReceivedData.s).toBe("string");
            expect(typeof tempReceivedData.p).toBe("number");
            expect(typeof tempReceivedData.ch).toBe("number");
            expect(typeof tempReceivedData.d).toBe("number");
          } else {
            console.log("Timeout waiting for BIST delayed data");
          }
        } catch (error) {
          receivedError = error as Error;
          console.log("Received delayed error:", receivedError.message);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("GetOrderbookForBIST", () => {
    it(
      "should receive BIST orderbook data",
      async () => {
        const symbols = ["AKBNK"];
        let receivedData: OrderbookLiveData | null = null;
        let receivedError: Error | null = null;

        const lc = client.getOrderbookForBIST(symbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout waiting for orderbook data")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                console.log(data);
                receivedData = data;
                break;
              }
            } catch (error) {
              console.log("Error in orderbook data stream:", error);
            }
          })();

          await Promise.race([dataPromise, timeoutPromise]);

          if (receivedData != null) {
            const tempReceivedData = receivedData as OrderbookLiveData;
            console.log("Received BIST orderbook data:", tempReceivedData);
            expect(tempReceivedData.symbol).toBeDefined();
            expect(typeof tempReceivedData.symbol).toBe("string");

            if (tempReceivedData.updated != null) {
              expect(Array.isArray(tempReceivedData.updated)).toBe(true);

              if (tempReceivedData.updated.length > 0) {
                const firsData = tempReceivedData.updated[0];
                console.log("updated first data:", firsData)
                expect(typeof firsData.level).toBe("number");
                expect(typeof firsData.vol).toBe("number");
                expect(typeof firsData.orders).toBe("number");
                expect(typeof firsData.p).toBe("number");
                expect(typeof firsData.side).toBe("string");
              }
            }

            if (tempReceivedData.deleted != null) {
              expect(Array.isArray(tempReceivedData.deleted)).toBe(true);

              if (tempReceivedData.deleted.length > 0) {
                const firsData = tempReceivedData.deleted[0];
                console.log("deleted first data:", firsData)
                expect(typeof firsData.level).toBe("number");
                expect(typeof firsData.side).toBe("string");
              }
            }
          } else {
            console.log("Timeout waiting for BIST orderbook data");
          }
        } catch (error) {
          receivedError = error as Error;
          console.log("Received orderbook error:", receivedError.message);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("Client Methods", () => {
    it(
      "should work with client methods",
      async () => {
        const symbols = ["THYAO"];
        let receivedData: any = null;

        const lc = client.getLivePriceForBIST(symbols);
        activeConnections.push(lc);

        try {
          const receiveChan = lc.receive();

          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });

          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData = data;
                break;
              }
            } catch (error) {
              console.log("Error in client methods test:", error);
            }
          })();

          await Promise.race([dataPromise, timeoutPromise]);

          if (receivedData) {
            expect(receivedData.s).toBeDefined();
            expect(typeof receivedData.s).toBe("string");
          }
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("GetClientWebsocketUrl", () => {
    it(
      "should return a websocket url string",
      async () => {
        const resp = await client.getClientWebsocketUrl("test-integration-user", [LivePriceFeed.LiveBist]);
        expect(typeof resp).toBe("string");
        expect(resp.length).toBeGreaterThan(0);
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("GetWebsocketUsageForMonth", () => {
    it(
      "should return an array of usage data",
      async () => {
        const resp = await client.getWebsocketUsageForMonth(1, 2025, LivePriceFeed.LiveBist);
        expect(Array.isArray(resp)).toBe(true);
        if (resp.length > 0) {
          expect(typeof resp[0].externalUserID).toBe("string");
          expect(typeof resp[0].uniqueDeviceCount).toBe("number");
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });

  describe("Mock Tests (Data Injection)", () => {
    let mockClient: LivePriceClient;
    let cli: { request: jest.Mock };

    beforeEach(() => {
      cli = { request: jest.fn() };
      const config = (global as any).testSuite.config as LaplaceConfiguration;
      const mockLogger: Logger = {
        info: jest.fn(),
        error: jest.fn(),
        warn: jest.fn(),
        debug: jest.fn(),
      } as unknown as Logger;
      mockClient = new LivePriceClient(config, mockLogger, cli as any);
    });

    describe("getClientWebsocketUrl", () => {
      test("calls correct endpoint, sends correct body, and returns url string", async () => {
        cli.request.mockResolvedValueOnce({ data: { url: "wss://example.com/ws" } });

        const resp = await mockClient.getClientWebsocketUrl("user123", [LivePriceFeed.LiveBist, LivePriceFeed.LiveUs]);

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];
        expect(call.method).toBe("POST");
        expect(call.url).toBe("/api/v2/ws/url");
        expect(call.data).toEqual({ externalUserId: "user123", feeds: [LivePriceFeed.LiveBist, LivePriceFeed.LiveUs] });
        expect(resp).toBe("wss://example.com/ws");
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Unauthorized"));

        await expect(mockClient.getClientWebsocketUrl("user123", [LivePriceFeed.LiveBist])).rejects.toThrow("Unauthorized");
        expect(cli.request).toHaveBeenCalledTimes(1);
      });
    });

    describe("getWebsocketUsageForMonth", () => {
      test("calls correct endpoint with correct query params and returns usage data", async () => {
        const mockUsage = [
          { externalUserID: "user123", firstConnectionTime: new Date("2024-01-15"), uniqueDeviceCount: 3 },
        ];
        cli.request.mockResolvedValueOnce({ data: mockUsage });

        const resp = await mockClient.getWebsocketUsageForMonth(1, 2024, LivePriceFeed.LiveBist);

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];
        expect(call.method).toBe("GET");
        expect(call.url).toBe("/api/v1/ws/report");
        expect(call.params).toEqual({ month: 1, year: 2024, feedType: LivePriceFeed.LiveBist });
        expect(resp).toEqual(mockUsage);
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Forbidden"));

        await expect(mockClient.getWebsocketUsageForMonth(1, 2024, LivePriceFeed.LiveBist)).rejects.toThrow("Forbidden");
        expect(cli.request).toHaveBeenCalledTimes(1);
      });
    });

    describe("sendWebsocketEvent", () => {
      test("calls correct endpoint with request body", async () => {
        cli.request.mockResolvedValueOnce({ data: undefined });

        const request = { externalUserID: "user123", event: { type: "test" }, transient: true };
        await mockClient.sendWebsocketEvent(request);

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];
        expect(call.method).toBe("POST");
        expect(call.url).toBe("/api/v1/ws/event");
        expect(call.data).toEqual(request);
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Bad request"));

        await expect(mockClient.sendWebsocketEvent({ event: { type: "test" } })).rejects.toThrow("Bad request");
      });
    });

    describe("revokeWebsocketConnection", () => {
      test("calls correct endpoint with id path param", async () => {
        cli.request.mockResolvedValueOnce({ data: {} });

        await mockClient.revokeWebsocketConnection("abc-123");

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];
        expect(call.method).toBe("POST");
        expect(call.url).toBe("/api/v1/ws/user/revoke/abc-123");
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Not found"));

        await expect(mockClient.revokeWebsocketConnection("bad-id")).rejects.toThrow("Not found");
      });
    });
  });

  describe("GetBidAskForBIST", () => {
    it(
      "should receive BIST bid/ask data",
      async () => {
        const symbols = ["AKBNK"];
        let receivedData: BISTBidAskStreamData | null = null;
        let receivedError: Error | null = null;
  
        const lc = client.getBidAskForBIST(symbols);
        activeConnections.push(lc);
  
        try {
          const receiveChan = lc.receive();
  
          const timeoutPromise = new Promise<void>((_, reject) => {
            const timeout = setTimeout(
              () => reject(new Error("Timeout waiting for bid/ask data")),
              TEST_CONSTANTS.MAIN_TIMEOUT
            );
            activeTimeouts.push(timeout);
          });
  
          const dataPromise = (async () => {
            try {
              for await (const data of receiveChan) {
                receivedData = data;
                break;
              }
            } catch (error) {
              console.log("Error in bid/ask data stream:", error);
            }
          })();
  
          await Promise.race([dataPromise, timeoutPromise]);
  
          if (receivedData != null) {
            const tempReceivedData = (receivedData as BISTBidAskStreamData).d;
            console.log("Received BIST bid/ask data:", tempReceivedData);
            expect(tempReceivedData.s).toBeDefined();
            expect(typeof tempReceivedData.s).toBe("string");
            expect(typeof tempReceivedData.d).toBe("number");
            expect(typeof tempReceivedData.ask).toBe("number");
            expect(typeof tempReceivedData.bid).toBe("number");
          } else {
            console.log("Timeout waiting for BIST bid/ask data");
          }
        } catch (error) {
          receivedError = error as Error;
          console.log("Received bid/ask error:", receivedError.message);
        } finally {
          lc.close();
        }
      },
      TEST_CONSTANTS.JEST_TIMEOUT
    );
  });
});
