import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { PlexSourceScreen } from './PlexSourceScreen';

function json(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json' }, status });
}

interface PlexState {
  isAuthorized: boolean;
  isLocked: boolean;
  servers: Array<{ id: number; name: string; address: string }>;
}

// A URL-keyed fetch stub. `stateRef.current` lets a test flip the polled Plex state over time; the
// counter proves the poll kept running (a stop would freeze the count).
function installFetch(stateRef: { current: PlexState }, counter: { state: number }) {
  return vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
    const method = (init?.method ?? 'GET').toUpperCase();

    if (url.endsWith('/api/v1/media-sources/plex/pin-flow') && method === 'POST') {
      return Promise.resolve(json({ authUrl: 'https://plex.tv/link?code=ABCD' }));
    }
    if (url.endsWith('/api/v1/media-sources/plex/sign-out') && method === 'POST') {
      return Promise.resolve(new Response(null, { status: 204 }));
    }
    if (url.includes('/refresh-libraries') && method === 'POST') {
      return Promise.resolve(new Response(null, { status: 204 }));
    }
    if (url.endsWith('/api/v1/media-sources/plex') && method === 'GET') {
      counter.state += 1;
      return Promise.resolve(json(stateRef.current));
    }
    return Promise.resolve(json({}, 404));
  });
}

describe('PlexSourceScreen', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  afterEach(() => {
    cleanup();
    vi.useRealTimers();
  });

  it('pin flow: waiting → finalizing → success, and keeps polling at authorized&locked (does NOT stop)', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    // Polls now read "locked, not authorized" = waiting.
    stateRef.current = { isAuthorized: false, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/Waiting for you to authorize/i)).toBeTruthy());

    await vi.advanceTimersByTimeAsync(2000);
    expect(screen.getByText(/Waiting for you to authorize/i)).toBeTruthy();

    // Authorized but STILL LOCKED = finalizing. The terminal signal is the lock releasing, so the
    // screen must keep polling here (design §C1 / finding 5): no servers shown, count keeps rising.
    stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
    await vi.advanceTimersByTimeAsync(2000);
    expect(screen.getByText(/discovering your Plex servers/i)).toBeTruthy();
    expect(screen.queryByRole('heading', { name: 'Servers' })).toBeNull();

    const callsAtFinalizing = counter.state;
    await vi.advanceTimersByTimeAsync(2000);
    // Still finalizing after another interval — proves it did NOT stop at authorized&locked.
    expect(counter.state).toBeGreaterThan(callsAtFinalizing);
    expect(screen.getByText(/discovering your Plex servers/i)).toBeTruthy();

    // Lock releases while authorized = terminal success; servers appear and polling stops.
    stateRef.current = { isAuthorized: true, isLocked: false, servers: [{ id: 5, name: 'Home Server', address: 'http://plex:32400' }] };
    await vi.advanceTimersByTimeAsync(2000);
    await vi.waitFor(() => expect(screen.getByRole('heading', { name: 'Servers' })).toBeTruthy());
    expect(screen.getByText('Home Server')).toBeTruthy();

    const callsAfterSuccess = counter.state;
    await vi.advanceTimersByTimeAsync(6000);
    expect(counter.state).toBe(callsAfterSuccess); // polling stopped on success
  });

  it('pin flow: an ended flow without auth reports a timeout and stops polling', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    stateRef.current = { isAuthorized: false, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/Waiting for you to authorize/i)).toBeTruthy());

    // Flow ends without authorization (tab closed / plex.tv timed out).
    stateRef.current = { isAuthorized: false, isLocked: false, servers: [] };
    await vi.advanceTimersByTimeAsync(2000);
    await vi.waitFor(() => expect(screen.getByText(/Plex sign-in timed out/i)).toBeTruthy());

    const callsAfterTimeout = counter.state;
    await vi.advanceTimersByTimeAsync(6000);
    expect(counter.state).toBe(callsAfterTimeout); // stopped
  });

  it('pin flow: budget exhausted while still locked shows "still working" and stops', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    // Stay authorized-but-locked forever (a very large first sync legitimately holds the lock).
    stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/discovering your Plex servers|Waiting/i)).toBeTruthy());

    // Consume the whole 150s budget.
    await vi.advanceTimersByTimeAsync(152_000);
    await vi.waitFor(() => expect(screen.getByText(/Still working/i)).toBeTruthy());

    const callsAtBudget = counter.state;
    await vi.advanceTimersByTimeAsync(6000);
    expect(counter.state).toBe(callsAtBudget); // stopped, not a failure loop
  });

  it('renders a clickable fallback link when the popup is blocked', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue(null);

    render(<PlexSourceScreen />);
    await screen.findByRole('button', { name: 'Sign in to Plex' });

    stateRef.current = { isAuthorized: false, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));

    const link = await screen.findByRole('link', { name: /Open the Plex sign-in page/i });
    expect(link).toHaveAttribute('href', 'https://plex.tv/link?code=ABCD');
  });

  it('sign out shows the content-removal warning dialog and posts on confirm', async () => {
    const stateRef = {
      current: { isAuthorized: true, isLocked: false, servers: [{ id: 9, name: 'Living Room', address: 'http://plex:32400' }] } as PlexState
    };
    const counter = { state: 0 };
    const fetchSpy = installFetch(stateRef, counter);

    render(<PlexSourceScreen />);
    await screen.findByRole('heading', { name: 'Servers' });

    fireEvent.click(screen.getByRole('button', { name: 'Sign out of Plex' }));
    expect(await screen.findByText(/removes all synced Plex servers/i)).toBeTruthy();

    fireEvent.click(screen.getByRole('button', { name: 'Sign out' }));
    await vi.waitFor(() =>
      expect(fetchSpy.mock.calls.some(([input, init]) => String(input).endsWith('/sign-out') && (init?.method ?? '').toUpperCase() === 'POST')).toBe(true)
    );
  });

  // #345: a real pin flow reached isAuthorized:true, isLocked:false, servers:[] (a Plex account that
  // owns no eligible servers). The UI used to claim "Connected to Plex." while the Connection card said
  // "Not signed in.", offered Sign in, and hid Sign out — so the stored authorization was unremovable.
  it('pin flow ending authorized/unlocked with NO servers is coherent and recoverable', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    stateRef.current = { isAuthorized: false, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/Waiting for you to authorize/i)).toBeTruthy());

    // Authorized but STILL LOCKED = finalizing (discovery in progress). The empty-servers terminal
    // messaging must NOT appear here — while locked this is the normal "discovering" window, not
    // "no servers were found" (regression guard for the isLocked gate).
    stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
    await vi.advanceTimersByTimeAsync(2000);
    await vi.waitFor(() => expect(screen.getByText(/discovering your Plex servers/i)).toBeTruthy());
    expect(screen.queryByText(/Your Plex account is authorized/i)).toBeNull(); // no "sign out and retry" summary
    expect(screen.queryByText(/no eligible servers were discovered/i)).toBeNull();
    expect(screen.queryByRole('button', { name: 'Sign in to Plex' })).toBeNull();

    // Lock releases while authorized but discovery found nothing: the distinct terminal state.
    stateRef.current = { isAuthorized: true, isLocked: false, servers: [] };
    await vi.advanceTimersByTimeAsync(2000);

    // Pin-flow message distinguishes authorization from server discovery (does not claim connection).
    await vi.waitFor(() =>
      expect(screen.getByText('Signed in to Plex, but no eligible servers were discovered.')).toBeTruthy()
    );
    expect(screen.queryByText('Connected to Plex.')).toBeNull();
    // The explanatory summary is shown (distinct wording anchors it).
    expect(screen.getByText(/Your Plex account is authorized/i)).toBeTruthy();
    // No Servers card, and the Connection card + actions agree: Sign out offered, Sign in withheld.
    expect(screen.queryByRole('heading', { name: 'Servers' })).toBeNull();
    expect(screen.getByRole('button', { name: 'Sign out of Plex' })).toBeTruthy();
    expect(screen.queryByRole('button', { name: 'Sign in to Plex' })).toBeNull();

    // Polling stopped on the terminal state.
    const callsAfterTerminal = counter.state;
    await vi.advanceTimersByTimeAsync(6000);
    expect(counter.state).toBe(callsAfterTerminal);
  });

  it('authorized/unlocked/empty state loaded fresh (no pin flow) can sign out', async () => {
    // The reload path: no pinState, not polling — driven purely by the loaded RemoteMediaSourceState.
    const stateRef = { current: { isAuthorized: true, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    const fetchSpy = installFetch(stateRef, counter);

    render(<PlexSourceScreen />);

    // The explanatory summary and Sign-out action reflect "authorized, no servers" (not "Not signed in.").
    await screen.findByText(/Your Plex account is authorized/i);
    expect(screen.queryByRole('button', { name: 'Sign in to Plex' })).toBeNull();
    expect(screen.queryByText('Not signed in.')).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: 'Sign out of Plex' }));
    expect(await screen.findByText(/removes all synced Plex servers/i)).toBeTruthy();
    fireEvent.click(screen.getByRole('button', { name: 'Sign out' }));
    await vi.waitFor(() =>
      expect(fetchSpy.mock.calls.some(([input, init]) => String(input).endsWith('/sign-out') && (init?.method ?? '').toUpperCase() === 'POST')).toBe(true)
    );
  });

  // #345 (review follow-up): a large first sync legitimately still holds the lock past the poll
  // budget (budget-exhausted). The empty-servers "sign out and retry" messaging must NOT show — it
  // would contradict the "Still working…" status and urge aborting an in-progress sync.
  it('budget-exhausted while locked with no servers does not show the "no servers, sign out" messaging', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    // Authorized but locked forever (large first sync holds the lock).
    stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/discovering your Plex servers|Waiting/i)).toBeTruthy());

    await vi.advanceTimersByTimeAsync(152_000); // consume the whole 150s budget
    await vi.waitFor(() => expect(screen.getByText(/Still working/i)).toBeTruthy());

    // The lock is still held, so no terminal "no servers were found" messaging.
    expect(screen.queryByText(/Your Plex account is authorized/i)).toBeNull();
    expect(screen.queryByText(/no eligible servers were discovered/i)).toBeNull();
  });

  // #367: budget-exhausted told the user to "Use Refresh to check again", but the Servers card (the
  // only place Refresh lives) doesn't render with zero servers, so the instruction pointed at nothing.
  // The message now describes checking again generically, and a global "Check again" button re-checks
  // GET /api/v1/media-sources/plex without resuming the (already spent) timed poll loop.
  it('budget-exhausted with no servers offers a working "Check again" control instead of a dead Refresh reference', async () => {
    const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
    const counter = { state: 0 };
    installFetch(stateRef, counter);
    vi.spyOn(window, 'open').mockReturnValue({} as Window);
    vi.useFakeTimers();

    render(<PlexSourceScreen />);
    await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());

    stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
    fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
    await vi.waitFor(() => expect(screen.getByText(/discovering your Plex servers|Waiting/i)).toBeTruthy());

    await vi.advanceTimersByTimeAsync(152_000); // consume the whole 150s budget
    await vi.waitFor(() => expect(screen.getByText(/Still working/i)).toBeTruthy());

    // The message no longer references a "Refresh" control that isn't rendered (no Servers card yet).
    expect(screen.queryByText(/Use Refresh/i)).toBeNull();
    expect(screen.queryByRole('heading', { name: 'Servers' })).toBeNull();

    // A real, working "Check again" control is offered instead.
    const checkAgainButton = screen.getByRole('button', { name: 'Check again' });
    expect(checkAgainButton).toBeTruthy();

    const callsBeforeCheck = counter.state;

    // The next sync completes and a server is discovered by the time the user checks again.
    stateRef.current = { isAuthorized: true, isLocked: false, servers: [{ id: 3, name: 'Attic Server', address: 'http://plex:32400' }] };
    fireEvent.click(checkAgainButton);

    // Check again performs its own fetch (does not rely on the exhausted timed poll loop resuming).
    await vi.waitFor(() => expect(counter.state).toBeGreaterThan(callsBeforeCheck));
    await vi.waitFor(() => expect(screen.getByRole('heading', { name: 'Servers' })).toBeTruthy());
    expect(screen.getByText('Attic Server')).toBeTruthy();
    expect(screen.getByText('Connected to Plex.')).toBeTruthy();
  });
});
