import React, { useContext } from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { AccountContext } from './AccountContext';
import {
  ConnectionStatus,
  WebSocketContext,
  WebSocketProvider,
} from './WebSocketContext';

jest.mock('../../axiosApi', () => ({
  axiosInstance: {
    post: jest.fn(),
    defaults: { headers: { common: {} } },
  },
  applyAccessToken: jest.fn(),
}));

jest.mock('../auth/tokenStorage', () => ({
  setTokens: jest.fn().mockResolvedValue(undefined),
  getAccessToken: jest.fn(() => null),
  getRefreshToken: jest.fn(() => null),
}));

class MockWebSocket {
  static OPEN = 1;
  static CONNECTING = 0;
  static CLOSING = 2;
  static CLOSED = 3;
  static instances = [];

  constructor(url) {
    this.url = url;
    this.readyState = MockWebSocket.CONNECTING;
    this.sent = [];
    this.onopen = null;
    this.onclose = null;
    this.onerror = null;
    this.onmessage = null;
    MockWebSocket.instances.push(this);
  }

  send(data) {
    this.sent.push(data);
  }

  close() {
    this.readyState = MockWebSocket.CLOSED;
    if (this.onclose) {
      this.onclose({ code: 1000 });
    }
  }

  /** Test helper: simulate successful open. */
  open() {
    this.readyState = MockWebSocket.OPEN;
    if (this.onopen) {
      this.onopen({});
    }
  }

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

const Probe = () => {
  const [
    subscribe,
    ,
    ,
    sendMessage,
    isConnected,
    connectionStatus,
    reconnectGeneration,
  ] = useContext(WebSocketContext);

  React.useEffect(() => {
    subscribe('ACCOUNT_ID_test@example.com', () => {});
  }, [subscribe]);

  return (
    <div>
      <span data-testid="status">{connectionStatus}</span>
      <span data-testid="connected">{isConnected ? 'yes' : 'no'}</span>
      <span data-testid="reconnect-gen">{reconnectGeneration}</span>
      <button
        type="button"
        onClick={() => sendMessage('hi', 1, null, null, 'FAST')}
      >
        send
      </button>
    </div>
  );
};

const renderWithAccount = (account) =>
  render(
    <AccountContext.Provider value={{ account, setAccount: () => {} }}>
      <WebSocketProvider>
        <Probe />
      </WebSocketProvider>
    </AccountContext.Provider>,
  );

describe('WebSocketProvider', () => {
  const originalWebSocket = global.WebSocket;
  const originalEnv = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;

  beforeEach(() => {
    MockWebSocket.reset();
    global.WebSocket = MockWebSocket;
    process.env.REACT_APP_BACKEND_WS_API_BASE_URL = 'ws://test/ws/chat_again/';
    localStorage.clear();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
    global.WebSocket = originalWebSocket;
    process.env.REACT_APP_BACKEND_WS_API_BASE_URL = originalEnv;
    delete window.Capacitor;
  });

  it('stays disconnected when no account', async () => {
    renderWithAccount(undefined);
    expect(screen.getByTestId('status')).toHaveTextContent(
      ConnectionStatus.DISCONNECTED,
    );
    expect(MockWebSocket.instances).toHaveLength(0);
  });

  it('connects when account is present', async () => {
    renderWithAccount({ email: 'test@example.com' });

    await waitFor(() => {
      expect(MockWebSocket.instances.length).toBeGreaterThan(0);
    });

    await act(async () => {
      MockWebSocket.instances[0].open();
    });

    expect(screen.getByTestId('status')).toHaveTextContent(
      ConnectionStatus.CONNECTED,
    );
    expect(screen.getByTestId('connected')).toHaveTextContent('yes');
  });

  it('schedules reconnect with backoff after unexpected close', async () => {
    renderWithAccount({ email: 'test@example.com' });

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

    await act(async () => {
      MockWebSocket.instances[0].open();
    });
    expect(screen.getByTestId('status')).toHaveTextContent(
      ConnectionStatus.CONNECTED,
    );

    await act(async () => {
      MockWebSocket.instances[0].close();
    });

    expect(screen.getByTestId('status')).toHaveTextContent(
      ConnectionStatus.RECONNECTING,
    );

    await act(async () => {
      jest.advanceTimersByTime(35000);
    });

    await waitFor(() => {
      expect(MockWebSocket.instances.length).toBeGreaterThan(1);
    });
  });

  it('increments reconnectGeneration after drop and successful reopen', async () => {
    renderWithAccount({ email: 'test@example.com' });

    await waitFor(() => expect(MockWebSocket.instances.length).toBe(1));
    await act(async () => {
      MockWebSocket.instances[0].open();
    });
    expect(screen.getByTestId('reconnect-gen')).toHaveTextContent('0');

    await act(async () => {
      MockWebSocket.instances[0].close();
    });
    await act(async () => {
      jest.advanceTimersByTime(35000);
    });

    await waitFor(() => expect(MockWebSocket.instances.length).toBeGreaterThan(1));
    await act(async () => {
      MockWebSocket.instances[MockWebSocket.instances.length - 1].open();
    });

    await waitFor(() => {
      expect(screen.getByTestId('reconnect-gen')).toHaveTextContent('1');
    });
  });

  it('reconnects on visibility resume when socket is down', async () => {
    renderWithAccount({ email: 'test@example.com' });
    await waitFor(() => expect(MockWebSocket.instances.length).toBe(1));
    await act(async () => {
      MockWebSocket.instances[0].open();
    });

    // Simulate dead socket without going through close handler scheduling
    MockWebSocket.instances[0].readyState = MockWebSocket.CLOSED;
    MockWebSocket.instances[0].onclose = null;

    Object.defineProperty(document, 'visibilityState', {
      configurable: true,
      get: () => 'visible',
    });

    const before = MockWebSocket.instances.length;
    await act(async () => {
      document.dispatchEvent(new Event('visibilitychange'));
    });

    await waitFor(() => {
      expect(MockWebSocket.instances.length).toBeGreaterThan(before);
    });
  });

  it('sends chat payload when connected', async () => {
    renderWithAccount({ email: 'test@example.com' });
    await waitFor(() => expect(MockWebSocket.instances.length).toBe(1));
    await act(async () => {
      MockWebSocket.instances[0].open();
    });

    await act(async () => {
      screen.getByRole('button', { name: 'send' }).click();
    });

    const sent = MockWebSocket.instances[0].sent;
    expect(sent.length).toBeGreaterThan(0);
    const payload = JSON.parse(sent[sent.length - 1]);
    expect(payload.message).toBe('hi');
    expect(payload.email).toBe('test@example.com');
  });

  it('does not send empty or whitespace-only messages', async () => {
    let sendMessage;
    function Capture() {
      const ctx = useContext(WebSocketContext);
      sendMessage = ctx[3];
      return null;
    }

    render(
      <AccountContext.Provider
        value={{ account: { email: 'test@example.com' }, setAccount: () => {} }}
      >
        <WebSocketProvider>
          <Capture />
        </WebSocketProvider>
      </AccountContext.Provider>,
    );

    await waitFor(() => expect(MockWebSocket.instances.length).toBe(1));
    await act(async () => {
      MockWebSocket.instances[0].open();
    });

    const before = MockWebSocket.instances[0].sent.length;
    await act(async () => {
      expect(sendMessage('   ', 1, null, null, 'FAST')).toBe(false);
      expect(sendMessage('', 1, null, null, 'FAST')).toBe(false);
    });
    expect(MockWebSocket.instances[0].sent.length).toBe(before);
  });
});
