import { Logger } from "winston";
import axios from "axios";
import { LaplaceConfiguration } from "../utilities/configuration";
import {
  NewsClient,
  NewsType,
  NewsOrderBy,
} from "../client/news";
import "./client_test_suite";
import { Region, Locale } from "../client/collections";
import { SortDirection } from "../client/broker";

const mockNewsHighlightsResponse = {
  tech: [
    "Alphabet ve Amazon'un desteğiyle Anthropic, 2026 başlarında Hindistan'ın Bengaluru kentinde bir ofis açacak."
  ],
  other: [
    "ABD Yüksek Mahkemesi, Epic Games'in davası kapsamında Google'ın Play uygulamalarındaki değişikliği engellemeyecek."
  ],
  finance: [
    "Fifth Third Bank, Comerica'yı 10,9 milyar dolara satın alacak ve böylece ABD'nin 9. en büyük bankası olacak."
  ],
  consumer: [
    "Tesla, rekabet ortamında pazar payını geri almak için daha ucuz Model Y ve Model 3'ü piyasaya sürdü; duyuru hisseleri etkiledi."
  ],
  healthcare: [
    "İlaç üreticileri, Amgen ve Novo Nordisk'in de dahil olduğu şekilde, Trump'ın ilaç fiyatlarını düşürme planıyla uyumlu olarak tele-sağlık satışlarını artırıyor."
  ],
  energyAndUtilities: [
    "ABD Enerji Bakanlığı, Stellantis ve GM'ye verilen 1,1 milyar dolarlık hibeleri iptal edebilir."
  ],
  industrialsAndMaterials: [
    "Boeing, bir grevi sona erdirmek için IAM Sendikası ile geçici bir anlaşmaya vardı; detaylar açıklanmadı."
  ]
};

const mockNewsResponse = {
  items: [
    {
      url: "https://www.reuters.com/business/energy/commonwealth-lng-wants-more-time-build-planned-export-facility-louisiana-2025-10-07/",
      content: {
        title: "Commonwealth LNG wants more time to build planned export facility in Louisiana",
        content: [
          "Commonwealth LNG has requested a four-year extension from federal regulators to construct & begin exporting liquefied natural gas..."
        ],
        summary: [
          "Commonwealth LNG has requested a four-year extension from federal regulators..."
        ],
        description:
          "Commonwealth LNG has asked federal regulators for a four-year extension...",
        investorInsight:
          "What it means for investors: The extension request could postpone..."
      },
      sectors: { name: "Energy", meanType: 9, newsCount: 1 },
      tickers: [{ id: "6203d1ba1e674875275558f7", name: "EQT Corp", symbol: "EQT" }],
      imageUrl: "",
      createdAt: "2025-10-07T17:10:01.560644Z",
      publisher: { name: "Reuters", logoUrl: null },
      timestamp: "2025-10-07T16:50:16Z",
      categories: { name: "Sector News", newsCount: 1, categoryType: "StockSpesific" },
      industries: { name: "Oil/Gas (Production and Exploration)", meanType: 78 },
      publisherUrl: "Reuters",
      qualityScore: 0,
      relatedTickers: [{ id: "6203d1ba1e674875275558f7", name: "EQT Corp", symbol: "EQT" }]
    }
  ],
  recordCount: 352
};

const mockNewsV2Response = {
  items: mockNewsResponse.items.map(({ relatedTickers, ...rest }) => rest),
  recordCount: mockNewsResponse.recordCount
};

describe("NewsClient", () => {
  let client: NewsClient;

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

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

  describe("Integration Tests", () => {
    jest.setTimeout(60_000);

    test("getHighlights returns valid data", async () => {
      const resp = await client.getHighlights(Region.Us, Locale.Tr);

      expect(resp).toBeDefined();

      expect(Array.isArray(resp.consumer)).toBe(true);
      expect(Array.isArray(resp.energyAndUtilities)).toBe(true);
      expect(Array.isArray(resp.finance)).toBe(true);
      expect(Array.isArray(resp.healthcare)).toBe(true);
      expect(Array.isArray(resp.industrialsAndMaterials)).toBe(true);
      expect(Array.isArray(resp.tech)).toBe(true);
      expect(Array.isArray(resp.other)).toBe(true);

      const first = resp.tech?.[0];
      if (first != null) expect(typeof first).toBe("string");
    });

    test("getNews returns valid paginated data", async () => {
      const resp = await client.getNews(
        Region.Us,
        Locale.Tr,
        NewsType.BRIEFS,
        0,
        10,
        NewsOrderBy.TIMESTAMP,
        SortDirection.Desc
      );

      expect(resp).toBeDefined();
      expect(typeof resp.recordCount).toBe("number");
      expect(resp.recordCount).toBeGreaterThanOrEqual(0);
      expect(Array.isArray(resp.items)).toBe(true);

      if (resp.items.length > 0) {
        const n = resp.items[0];

        expect(typeof n.url).toBe("string");
        expect(typeof n.imageUrl).toBe("string");
        expect(typeof n.timestamp).toBe("string");
        expect(typeof n.publisherUrl).toBe("string");
        expect(typeof n.qualityScore).toBe("number");
        expect(typeof n.createdAt).toBe("string");

        expect(n.publisher).toBeDefined();
        expect(typeof n.publisher.name).toBe("string");
        expect(
          typeof n.publisher.logoUrl === "string" || n.publisher.logoUrl == null
        ).toBe(true);

        expect(Array.isArray(n.relatedTickers)).toBe(true);
        if (n.relatedTickers.length > 0) {
          const t = n.relatedTickers[0];
          expect(typeof t.id).toBe("string");
          expect(typeof t.name).toBe("string");
          expect(typeof t.symbol === "string" || t.symbol == null).toBe(true);
        }

        if (n.tickers != null) {
          expect(Array.isArray(n.tickers)).toBe(true);
        }

        if (n.categories != null) {
          expect(typeof n.categories.name).toBe("string");
          expect(typeof n.categories.newsCount).toBe("number");
          expect(
            typeof n.categories.categoryType === "string" ||
            n.categories.categoryType == null ||
            n.categories.categoryType === undefined
          ).toBe(true);
          expect(
            typeof n.categories.meanType === "number" ||
            n.categories.meanType == null ||
            n.categories.meanType === undefined
          ).toBe(true);
        }

        if (n.sectors != null) {
          expect(typeof n.sectors.name).toBe("string");
          expect(typeof n.sectors.newsCount).toBe("number");
          expect(
            typeof n.sectors.categoryType === "string" ||
            n.sectors.categoryType == null ||
            n.sectors.categoryType === undefined
          ).toBe(true);
          expect(
            typeof n.sectors.meanType === "number" ||
            n.sectors.meanType == null ||
            n.sectors.meanType === undefined
          ).toBe(true);
        }

        if (n.industries != null) {
          expect(typeof n.industries.name).toBe("string");
          expect(typeof n.industries.meanType).toBe("number");
        }

        if (n.content != null) {
          expect(typeof n.content.title).toBe("string");
          expect(typeof n.content.description).toBe("string");
          expect(Array.isArray(n.content.content)).toBe(true);
          expect(Array.isArray(n.content.summary)).toBe(true);
          expect(typeof n.content.investorInsight).toBe("string");
        }
      }
    });

    test("getNewsV2 returns valid paginated data", async () => {
      const resp = await client.getNewsV2(
        Region.Us,
        Locale.Tr,
        NewsType.BRIEFS,
        0,
        10,
        NewsOrderBy.TIMESTAMP,
        SortDirection.Desc
      );

      expect(resp).toBeDefined();
      expect(typeof resp.recordCount).toBe("number");
      expect(resp.recordCount).toBeGreaterThanOrEqual(0);
      expect(Array.isArray(resp.items)).toBe(true);

      if (resp.items.length > 0) {
        const n = resp.items[0];

        expect(typeof n.url).toBe("string");
        expect(typeof n.imageUrl).toBe("string");
        expect(typeof n.timestamp).toBe("string");
        expect(typeof n.publisherUrl).toBe("string");
        expect(typeof n.qualityScore).toBe("number");
        expect(typeof n.createdAt).toBe("string");

        expect((n as any).relatedTickers).toBeUndefined();

        expect(n.publisher).toBeDefined();
        expect(typeof n.publisher.name).toBe("string");
      }
    });

    test("streamNews yields item before timeout or throws gracefully if none arrive", async () => {
      let newsItemsReceived = 0;
      const { events, cancel } = client.streamNews(Region.Us, Locale.Tr);

      const receivePromise = (async () => {
        for await (const items of events) {
          if (items && items.length > 0) {
            newsItemsReceived += items.length;
            break;
          }
        }
      })();

      const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 8000));
      await Promise.race([receivePromise, timeoutPromise]);
      cancel();
      expect(newsItemsReceived).toBeGreaterThanOrEqual(0);
    });
  });

  describe("Mock Tests", () => {
    let client: NewsClient;
    let cli: { request: jest.Mock };

    beforeEach(() => {
      cli = { request: jest.fn() };

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

      client = new NewsClient(config, logger, cli as any);
    });

    describe("getHighlights", () => {
      test("calls correct endpoint/params and matches raw response", async () => {
        cli.request.mockResolvedValueOnce({ data: mockNewsHighlightsResponse });

        const resp = await client.getHighlights(Region.Tr, Locale.Tr);

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];

        expect(call.method).toBe("GET");
        expect(call.url).toBe("/api/v1/news/highlights");
        expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr });

        expect(resp.consumer).toEqual(mockNewsHighlightsResponse.consumer);
        expect(resp.energyAndUtilities).toEqual(mockNewsHighlightsResponse.energyAndUtilities);
        expect(resp.finance).toEqual(mockNewsHighlightsResponse.finance);
        expect(resp.healthcare).toEqual(mockNewsHighlightsResponse.healthcare);
        expect(resp.industrialsAndMaterials).toEqual(mockNewsHighlightsResponse.industrialsAndMaterials);
        expect(resp.tech).toEqual(mockNewsHighlightsResponse.tech);
        expect(resp.other).toEqual(mockNewsHighlightsResponse.other);
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Failed to fetch highlights"));

        await expect(client.getHighlights(Region.Tr, Locale.Tr)).rejects.toThrow(
          "Failed to fetch highlights"
        );

        expect(cli.request).toHaveBeenCalledTimes(1);
      });
    });

    describe("getNews", () => {
      test("calls correct endpoint/params and matches raw response", async () => {
        cli.request.mockResolvedValueOnce({ data: mockNewsResponse });

        const resp = await client.getNews(
          Region.Tr,
          Locale.Tr,
          NewsType.BRIEFS,
          1,
          10,
          NewsOrderBy.TIMESTAMP,
          SortDirection.Desc,
          undefined
        );

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];

        expect(call.method).toBe("GET");
        expect(call.url).toBe("/api/v1/news");
        expect(call.params).toEqual({
          region: Region.Tr,
          locale: Locale.Tr,
          newsType: NewsType.BRIEFS,
          page: 1,
          size: 10,
          orderBy: NewsOrderBy.TIMESTAMP,
          orderByDirection: SortDirection.Desc
        });

        expect(resp.recordCount).toBe(352);
        expect(resp.items).toHaveLength(1);

        const n = resp.items[0];

        expect(n.url).toBe(mockNewsResponse.items[0].url);
        expect(n.imageUrl).toBe(mockNewsResponse.items[0].imageUrl);
        expect(n.timestamp).toBe(mockNewsResponse.items[0].timestamp);
        expect(n.publisherUrl).toBe(mockNewsResponse.items[0].publisherUrl);
        expect(n.qualityScore).toBe(mockNewsResponse.items[0].qualityScore);
        expect(n.createdAt).toBe(mockNewsResponse.items[0].createdAt);

        expect(n.publisher.name).toBe(mockNewsResponse.items[0].publisher.name);
        expect(n.publisher.logoUrl).toBeNull();

        expect(n.relatedTickers).toHaveLength(1);
        expect(n.relatedTickers[0].id).toBe(mockNewsResponse.items[0].relatedTickers[0].id);
        expect(n.relatedTickers[0].name).toBe(mockNewsResponse.items[0].relatedTickers[0].name);
        expect(n.relatedTickers[0].symbol).toBe(mockNewsResponse.items[0].relatedTickers[0].symbol);

        expect(n.tickers).toHaveLength(1);
        expect(n.tickers![0].symbol).toBe("EQT");

        expect(n.categories?.name).toBe(mockNewsResponse.items[0].categories.name);
        expect(n.categories?.newsCount).toBe(mockNewsResponse.items[0].categories.newsCount);
        expect(n.categories?.categoryType).toBe(mockNewsResponse.items[0].categories.categoryType);

        expect(n.sectors?.name).toBe(mockNewsResponse.items[0].sectors.name);
        expect(n.sectors?.newsCount).toBe(mockNewsResponse.items[0].sectors.newsCount);
        expect(n.sectors?.meanType).toBe(mockNewsResponse.items[0].sectors.meanType);

        expect(n.industries?.name).toBe(mockNewsResponse.items[0].industries.name);
        expect(n.industries?.meanType).toBe(mockNewsResponse.items[0].industries.meanType);

        expect(n.content?.title).toBe(mockNewsResponse.items[0].content.title);
        expect(n.content?.description).toBe(mockNewsResponse.items[0].content.description);
        expect(n.content?.content).toEqual(mockNewsResponse.items[0].content.content);
        expect(n.content?.summary).toEqual(mockNewsResponse.items[0].content.summary);
        expect(n.content?.investorInsight).toBe(mockNewsResponse.items[0].content.investorInsight);
      });

      test("does not send optional params when undefined", async () => {
        cli.request.mockResolvedValueOnce({ data: mockNewsResponse });

        await client.getNews(Region.Tr, Locale.Tr);

        const call = cli.request.mock.calls[0][0];
        expect(call.params).toEqual({
          region: Region.Tr,
          locale: Locale.Tr
        });
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Failed to fetch news"));

        await expect(
          client.getNews(
            Region.Tr,
            Locale.Tr,
            NewsType.REUTERS,
            0,
            10,
            NewsOrderBy.TIMESTAMP,
            SortDirection.Desc
          )
        ).rejects.toThrow("Failed to fetch news");

        expect(cli.request).toHaveBeenCalledTimes(1);
      });
    });

    describe("getNewsV2", () => {
      test("calls correct endpoint/params and matches raw response", async () => {
        cli.request.mockResolvedValueOnce({ data: mockNewsV2Response });

        const resp = await client.getNewsV2(
          Region.Tr,
          Locale.Tr,
          NewsType.BRIEFS,
          1,
          10,
          NewsOrderBy.TIMESTAMP,
          SortDirection.Desc,
          undefined
        );

        expect(cli.request).toHaveBeenCalledTimes(1);
        const call = cli.request.mock.calls[0][0];

        expect(call.method).toBe("GET");
        expect(call.url).toBe("/api/v2/news");
        expect(call.params).toEqual({
          region: Region.Tr,
          locale: Locale.Tr,
          newsType: NewsType.BRIEFS,
          page: 1,
          size: 10,
          orderBy: NewsOrderBy.TIMESTAMP,
          orderByDirection: SortDirection.Desc
        });

        expect(resp.recordCount).toBe(352);
        expect(resp.items).toHaveLength(1);

        const n = resp.items[0];

        expect((n as any).relatedTickers).toBeUndefined();
        expect(n.url).toBe(mockNewsV2Response.items[0].url);
      });

      test("does not send optional params when undefined", async () => {
        cli.request.mockResolvedValueOnce({ data: mockNewsV2Response });

        await client.getNewsV2(Region.Tr, Locale.Tr);

        const call = cli.request.mock.calls[0][0];
        expect(call.params).toEqual({
          region: Region.Tr,
          locale: Locale.Tr
        });
      });

      test("bubbles up request error", async () => {
        cli.request.mockRejectedValueOnce(new Error("Failed to fetch news v2"));

        await expect(
          client.getNewsV2(
            Region.Tr,
            Locale.Tr,
            NewsType.REUTERS,
            0,
            10,
            NewsOrderBy.TIMESTAMP,
            SortDirection.Desc
          )
        ).rejects.toThrow("Failed to fetch news v2");

        expect(cli.request).toHaveBeenCalledTimes(1);
      });
    });

    describe("streamNews", () => {
      test("calls correct endpoint/params and correctly yields stream entities", async () => {
        const eventsList: any[] = [];

        // Mock get response to return a readable stream
        const mockStreamData = [
          "data: " + JSON.stringify([{ url: "http://example.com/stream-news-1", publiser: { name: "test-publisher" } }]) + "\n\n",
          "data: " + JSON.stringify([{ url: "http://example.com/stream-news-2", publiser: { name: "test-publisher-2" } }]) + "\n\n",
        ];

        const mockAsyncIterator = {
          async *[Symbol.asyncIterator]() {
            for (const chunk of mockStreamData) {
              yield new TextEncoder().encode(chunk);
            }
          }
        };

        const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({
          data: mockAsyncIterator
        });

        const { events, cancel } = client.streamNews(Region.Us, Locale.Tr);

        for await (const newsList of events) {
          eventsList.push(newsList);
        }

        expect(axiosGetSpy).toHaveBeenCalledTimes(1);
        const callArgs = axiosGetSpy.mock.calls[0];
        expect(callArgs[0]).toBe(`${client["baseUrl"]}/api/v1/news/stream?locale=tr&region=us`);
        expect(callArgs[1]?.responseType).toBe('stream');

        expect(eventsList).toHaveLength(2);
        expect(eventsList[0][0].url).toBe("http://example.com/stream-news-1");
        expect(eventsList[1][0].url).toBe("http://example.com/stream-news-2");

        cancel();
        axiosGetSpy.mockRestore();
      });

      test("calls correct endpoint with optional parameters", async () => {
        const mockAsyncIterator = {
          async *[Symbol.asyncIterator]() {
            yield new TextEncoder().encode("data: " + JSON.stringify([]) + "\n\n");
          }
        };

        const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({
          data: mockAsyncIterator
        });

        const { events, cancel } = client.streamNews(Region.Us, Locale.En, ["tech"], ["AAPL"], ["category"], ["software"]);

        for await (const _ of events) {
          break;
        }

        expect(axiosGetSpy).toHaveBeenCalledTimes(1);
        const callArgs = axiosGetSpy.mock.calls[0];
        expect(callArgs[0]).toBe(`${client["baseUrl"]}/api/v1/news/stream?locale=en&region=us&sectors=tech&tickers=AAPL&categories=category&industries=software`);

        cancel();
        axiosGetSpy.mockRestore();
      });
    });
  });
});
