import React from "react";
import { render, screen, waitFor, act } from "@testing-library/react";
import SSEProvider, { SSEContext, useSSE } from "../../context/SSEContext";
import { AuthContext } from "../../context/AuthContext";
import axios from "axios";

// Mock axios
jest.mock("axios");

// Mock EventSource
class MockEventSource {
  constructor(url) {
    this.url = url;
    this.onopen = null;
    this.onerror = null;
    this.onmessage = null;
    this.readyState = 0;
    MockEventSource.instances.push(this);
  }

  close() {
    this.readyState = 2;
  }

  static instances = [];
  static reset() {
    MockEventSource.instances = [];
  }
}

global.EventSource = MockEventSource;

describe("SSEContext", () => {
  let mockAuthContext;

  beforeEach(() => {
    jest.clearAllMocks();
    MockEventSource.reset();

    mockAuthContext = {
      auth: {
        isAuthenticated: true,
        token: "mock-token",
        user: { id: "user123", name: "Test User" },
      },
    };

    // Mock axios responses
    axios.post.mockResolvedValue({ data: { subscribed: [] } });
  });

  afterEach(() => {
    jest.clearAllTimers();
  });

  const renderWithAuth = (children, authValue = mockAuthContext) => {
    return render(
      <AuthContext.Provider value={authValue}>
        <SSEProvider>{children}</SSEProvider>
      </AuthContext.Provider>
    );
  };

  describe("Connection Lifecycle", () => {
    it("renders children correctly", () => {
      renderWithAuth(<div>Test Content</div>);
      expect(screen.getByText("Test Content")).toBeInTheDocument();
    });

    it("connects to SSE stream when authenticated", async () => {
      renderWithAuth(<div>Test</div>);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
        expect(MockEventSource.instances[0].url).toContain("/api/sse/stream");
        expect(MockEventSource.instances[0].url).toContain("token=mock-token");
      });
    });

    it("does not connect when not authenticated", () => {
      const unauthContext = {
        auth: {
          isAuthenticated: false,
          token: null,
          user: null,
        },
      };

      renderWithAuth(<div>Test</div>, unauthContext);

      expect(MockEventSource.instances.length).toBe(0);
    });

    it("sets connected state to true on open", async () => {
      const TestComponent = () => {
        const { connected } = useSSE();
        return <div>{connected ? "Connected" : "Disconnected"}</div>;
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Trigger onopen
      act(() => {
        MockEventSource.instances[0].onopen();
      });

      await waitFor(() => {
        expect(screen.getByText("Connected")).toBeInTheDocument();
      });
    });

    it("handles connection errors", async () => {
      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});

      renderWithAuth(<div>Test</div>);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Trigger onerror
      act(() => {
        MockEventSource.instances[0].onerror(new Error("Connection error"));
      });

      await waitFor(() => {
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          "SSE: Connection error:",
          expect.any(Error)
        );
      });

      consoleErrorSpy.mockRestore();
    });

    it("disconnects when user logs out", async () => {
      const { rerender } = renderWithAuth(<div>Test</div>);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      const eventSource = MockEventSource.instances[0];
      const closeSpy = jest.spyOn(eventSource, "close");

      // Update auth to unauthenticated
      const unauthContext = {
        auth: {
          isAuthenticated: false,
          token: null,
          user: null,
        },
      };

      rerender(
        <AuthContext.Provider value={unauthContext}>
          <SSEProvider>
            <div>Test</div>
          </SSEProvider>
        </AuthContext.Provider>
      );

      await waitFor(() => {
        expect(closeSpy).toHaveBeenCalled();
      });
    });

  it("closes connection on unmount", async () => {
    const { unmount } = renderWithAuth(<div>Test</div>);

    await waitFor(() => {
      expect(MockEventSource.instances.length).toBe(1);
    });

    const eventSource = MockEventSource.instances[0];
    const originalClose = eventSource.close;
    eventSource.close = jest.fn(); // Replace close method with spy

    unmount();

    // Wait for cleanup to complete (useEffect cleanup runs asynchronously)
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 50));
    });

    expect(eventSource.close).toHaveBeenCalled();
    
    // Restore original close method
    eventSource.close = originalClose;
  });
  });

  describe("Event Handler Registration", () => {
    it("registers event handlers with on()", async () => {
      const TestComponent = () => {
        const { on } = useSSE();
        React.useEffect(() => {
          const handler = jest.fn();
          on("testEvent", handler);
        }, [on]);
        return <div>Test</div>;
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(screen.getByText("Test")).toBeInTheDocument();
      });
    });

    it("unregisters event handlers with off()", async () => {
      const TestComponent = () => {
        const { on, off } = useSSE();
        React.useEffect(() => {
          const handler = jest.fn();
          on("testEvent", handler);
          return () => {
            off("testEvent", handler);
          };
        }, [on, off]);
        return <div>Test</div>;
      };

      const { unmount } = renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(screen.getByText("Test")).toBeInTheDocument();
      });

      unmount();
    });

    it("calls registered handlers when event is received", async () => {
      const handler = jest.fn();

      const TestComponent = () => {
        const { on } = useSSE();
        React.useEffect(() => {
          on("testEvent", handler);
        }, [on]);
        return <div>Test</div>;
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Simulate receiving a message
      const eventData = {
        type: "testEvent",
        payload: { message: "Test message" },
      };

      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify(eventData),
        });
      });

      await waitFor(() => {
        expect(handler).toHaveBeenCalledWith({ message: "Test message" });
      });
    });

    it("handles multiple handlers for the same event type", async () => {
      const handler1 = jest.fn();
      const handler2 = jest.fn();

      const TestComponent = () => {
        const { on } = useSSE();
        React.useEffect(() => {
          on("testEvent", handler1);
          on("testEvent", handler2);
        }, [on]);
        return <div>Test</div>;
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Simulate receiving a message
      const eventData = {
        type: "testEvent",
        payload: { message: "Test message" },
      };

      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify(eventData),
        });
      });

      await waitFor(() => {
        expect(handler1).toHaveBeenCalledWith({ message: "Test message" });
        expect(handler2).toHaveBeenCalledWith({ message: "Test message" });
      });
    });
  });

  describe("Topic Subscription API", () => {
    it("subscribes to topics via API call", async () => {
      const TestComponent = () => {
        const { subscribe } = useSSE();
        return (
          <button onClick={() => subscribe(["events", "tasks"])}>
            Subscribe
          </button>
        );
      };

      renderWithAuth(<TestComponent />);

      const subscribeButton = screen.getByText("Subscribe");
      act(() => {
        subscribeButton.click();
      });

      await waitFor(() => {
        expect(axios.post).toHaveBeenCalledWith("/api/sse/subscribe", {
          topics: ["events", "tasks"],
        });
      });
    });

    it("unsubscribes from topics via API call", async () => {
      const TestComponent = () => {
        const { unsubscribe } = useSSE();
        return (
          <button onClick={() => unsubscribe(["events", "tasks"])}>
            Unsubscribe
          </button>
        );
      };

      renderWithAuth(<TestComponent />);

      const unsubscribeButton = screen.getByText("Unsubscribe");
      act(() => {
        unsubscribeButton.click();
      });

      await waitFor(() => {
        expect(axios.post).toHaveBeenCalledWith("/api/sse/unsubscribe", {
          topics: ["events", "tasks"],
        });
      });
    });

    it("does not subscribe when not authenticated", async () => {
      const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});

      const unauthContext = {
        auth: {
          isAuthenticated: false,
          token: null,
          user: null,
        },
      };

      const TestComponent = () => {
        const { subscribe } = useSSE();
        return (
          <button onClick={() => subscribe(["events"])}>Subscribe</button>
        );
      };

      renderWithAuth(<TestComponent />, unauthContext);

      const subscribeButton = screen.getByText("Subscribe");
      act(() => {
        subscribeButton.click();
      });

      await waitFor(() => {
        expect(consoleWarnSpy).toHaveBeenCalledWith(
          "SSE: Cannot subscribe - not authenticated"
        );
        expect(axios.post).not.toHaveBeenCalled();
      });

      consoleWarnSpy.mockRestore();
    });

    it("handles subscription errors gracefully", async () => {
      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
      axios.post.mockRejectedValueOnce(new Error("Network error"));

      const TestComponent = () => {
        const { subscribe } = useSSE();
        return (
          <button onClick={() => subscribe(["events"])}>Subscribe</button>
        );
      };

      renderWithAuth(<TestComponent />);

      const subscribeButton = screen.getByText("Subscribe");
      act(() => {
        subscribeButton.click();
      });

      await waitFor(() => {
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          "SSE: Error subscribing to topics:",
          expect.any(Error)
        );
      });

      consoleErrorSpy.mockRestore();
    });
  });

  describe("Notification State Management", () => {
    it("adds notifications to state when received", async () => {
      const TestComponent = () => {
        const { notifications } = useSSE();
        return (
          <div>
            {notifications.map((n) => (
              <div key={n.id}>{n.message}</div>
            ))}
          </div>
        );
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Simulate receiving a notification
      const eventData = {
        type: "notification",
        payload: { message: "New notification" },
      };

      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify(eventData),
        });
      });

      await waitFor(() => {
        expect(screen.getByText("New notification")).toBeInTheDocument();
      });
    });

    it("clears a specific notification", async () => {
      const TestComponent = () => {
        const { notifications, clearNotification } = useSSE();
        return (
          <div>
            {notifications.map((n) => (
              <div key={n.id}>
                {n.message}
                <button onClick={() => clearNotification(n.id)}>Clear</button>
              </div>
            ))}
          </div>
        );
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Add a notification
      const eventData = {
        type: "notification",
        payload: { message: "Test notification" },
      };

      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify(eventData),
        });
      });

      await waitFor(() => {
        expect(screen.getByText("Test notification")).toBeInTheDocument();
      });

      // Clear the notification
      const clearButton = screen.getByText("Clear");
      act(() => {
        clearButton.click();
      });

      await waitFor(() => {
        expect(screen.queryByText("Test notification")).not.toBeInTheDocument();
      });
    });

    it("clears all notifications", async () => {
      const TestComponent = () => {
        const { notifications, clearAllNotifications } = useSSE();
        return (
          <div>
            <button onClick={clearAllNotifications}>Clear All</button>
            {notifications.map((n) => (
              <div key={n.id}>{n.message}</div>
            ))}
          </div>
        );
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Add multiple notifications
      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify({
            type: "notification",
            payload: { message: "Notification 1" },
          }),
        });
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify({
            type: "notification",
            payload: { message: "Notification 2" },
          }),
        });
      });

      await waitFor(() => {
        expect(screen.getByText("Notification 1")).toBeInTheDocument();
        expect(screen.getByText("Notification 2")).toBeInTheDocument();
      });

      // Clear all notifications
      const clearAllButton = screen.getByText("Clear All");
      act(() => {
        clearAllButton.click();
      });

      await waitFor(() => {
        expect(screen.queryByText("Notification 1")).not.toBeInTheDocument();
        expect(screen.queryByText("Notification 2")).not.toBeInTheDocument();
      });
    });
  });

  describe("useSSE Hook", () => {
    it("throws error when used outside SSEProvider", () => {
      const TestComponent = () => {
        useSSE();
        return <div>Test</div>;
      };

      // Suppress console.error for this test
      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});

      expect(() => {
        render(<TestComponent />);
      }).toThrow("useSSE must be used within an SSEProvider");

      consoleErrorSpy.mockRestore();
    });

    it("returns SSE context value when used correctly", () => {
      const TestComponent = () => {
        const context = useSSE();
        return (
          <div>
            {context.connected ? "Connected" : "Disconnected"}
          </div>
        );
      };

      renderWithAuth(<TestComponent />);

      expect(screen.getByText("Disconnected")).toBeInTheDocument();
    });
  });

  describe("Error Handling", () => {
    it("handles malformed JSON messages gracefully", async () => {
      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});

      renderWithAuth(<div>Test</div>);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Send malformed JSON
      act(() => {
        MockEventSource.instances[0].onmessage({
          data: "invalid json",
        });
      });

      await waitFor(() => {
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          "SSE: Error parsing message:",
          expect.any(Error),
          "invalid json"
        );
      });

      consoleErrorSpy.mockRestore();
    });

    it("handles errors in event handlers gracefully", async () => {
      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
      const throwingHandler = jest.fn(() => {
        throw new Error("Handler error");
      });

      const TestComponent = () => {
        const { on } = useSSE();
        React.useEffect(() => {
          on("testEvent", throwingHandler);
        }, [on]);
        return <div>Test</div>;
      };

      renderWithAuth(<TestComponent />);

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBe(1);
      });

      // Trigger the handler
      act(() => {
        MockEventSource.instances[0].onmessage({
          data: JSON.stringify({
            type: "testEvent",
            payload: { message: "Test" },
          }),
        });
      });

      await waitFor(() => {
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          "SSE: Error in event handler for testEvent:",
          expect.any(Error)
        );
      });

      consoleErrorSpy.mockRestore();
    });
  });
});
