import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { navigateToPath } from '../routing';
import {
  ArtistDetailScreen,
  MovieDetailScreen,
  SeasonDetailScreen,
  ShowDetailScreen
} from './MediaDetailScreen';

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

const movie = {
  id: 5,
  title: 'The Movie',
  year: '1999',
  plot: 'A plot',
  genres: ['Drama'],
  tags: [],
  studios: [],
  contentRatings: [],
  languages: ['English'],
  actors: [{ id: 1, name: 'Actor', role: 'Role', thumb: '' }],
  directors: [],
  writers: [],
  path: '/media/movie.mkv',
  localPath: '/media/movie.mkv',
  state: 'Normal',
  poster: '/artwork/posters/p.jpg',
  fanArt: ''
};

function showDetail(mediaSourceKind: string) {
  return {
    id: 42,
    libraryId: 3,
    mediaSourceKind,
    title: 'The Show',
    year: '2001',
    plot: 'Show plot',
    genres: [],
    tags: [],
    studios: [],
    networks: [],
    contentRatings: [],
    languages: [],
    actors: [],
    poster: '',
    fanArt: ''
  };
}

const season = {
  id: 8,
  showId: 42,
  title: 'The Show',
  name: 'Season 1',
  year: '2001',
  poster: '',
  fanArt: ''
};

const artist = {
  id: 11,
  name: 'The Artist',
  disambiguation: '',
  biography: 'Bio',
  genres: [],
  styles: [],
  moods: [],
  languages: [],
  thumbnail: '',
  fanArt: ''
};

// Serves a detail record on its /api/v1/<kind>/<id> route, an empty child browse page, and stubs for
// the Add-to dialog loads so opening the plus menu never errors.
function mockDetailFetch(detail: unknown) {
  return vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
    const url = input.toString();
    if (url.startsWith('/api/v1/library/browse')) {
      return Promise.resolve(jsonResponse({ page: [], totalCount: 0 }));
    }
    if (url === '/api/v1/collections' || url === '/api/v1/playlists/groups' || url === '/api/v1/schedules') {
      return Promise.resolve(jsonResponse([]));
    }
    if (url.startsWith('/api/v1/libraries/') && url.includes('/scan-show')) {
      return Promise.resolve(new Response(null, { status: 200 }));
    }
    return Promise.resolve(jsonResponse(detail));
  });
}

describe('media detail screens', () => {
  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  it('renders a movie title, plot and cast', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(movie));

    render(<MovieDetailScreen id={5} />);

    await waitFor(() => expect(screen.getByText('The Movie')).toBeInTheDocument());
    expect(screen.getByText('A plot')).toBeInTheDocument();
    expect(screen.getByText('Actor')).toBeInTheDocument();
    expect(screen.getByText('Drama')).toBeInTheDocument();
  });

  it('shows a not-found message on 404', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ detail: 'nope' }, 404));

    render(<ShowDetailScreen id={99} />);

    await waitFor(() => expect(screen.getByText('Show not found.')).toBeInTheDocument());
  });

  it('offers an Add-to menu on the movie detail page', async () => {
    mockDetailFetch(movie);
    render(<MovieDetailScreen id={5} />);
    await waitFor(() => expect(screen.getByText('The Movie')).toBeInTheDocument());
    expect(screen.getByRole('button', { name: 'Add to…' })).toBeInTheDocument();
  });

  it('offers an Add-to menu on the season and artist detail pages', async () => {
    mockDetailFetch(season);
    const { unmount } = render(<SeasonDetailScreen id={8} />);
    await waitFor(() => expect(screen.getByText('The Show — Season 1')).toBeInTheDocument());
    expect(screen.getAllByRole('button', { name: 'Add to…' }).length).toBeGreaterThanOrEqual(1);
    unmount();

    mockDetailFetch(artist);
    render(<ArtistDetailScreen id={11} />);
    await waitFor(() => expect(screen.getByText('The Artist')).toBeInTheDocument());
    expect(screen.getByRole('button', { name: 'Add to…' })).toBeInTheDocument();
  });

  it('hides per-show scan for Local libraries and shows it for Plex', async () => {
    mockDetailFetch(showDetail('Local'));
    const { unmount } = render(<ShowDetailScreen id={42} />);
    await waitFor(() => expect(screen.getByText('The Show')).toBeInTheDocument());
    expect(screen.queryByRole('button', { name: 'Quick scan' })).toBeNull();
    unmount();

    const fetchSpy = mockDetailFetch(showDetail('Plex'));
    render(<ShowDetailScreen id={42} />);
    await waitFor(() => expect(screen.getByText('The Show')).toBeInTheDocument());
    expect(screen.getByRole('button', { name: 'Quick scan' })).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: 'Deep scan' }));
    await waitFor(() => {
      const scanCall = fetchSpy.mock.calls.find(([url]) => String(url) === '/api/v1/libraries/3/scan-show');
      expect(scanCall).toBeTruthy();
      const body = JSON.parse(String((scanCall![1] as RequestInit).body));
      expect(body).toMatchObject({ deepScan: true, showId: 42 });
    });
    await waitFor(() => expect(screen.getByText('Scan queued')).toBeInTheDocument());
  });

  it('exposes Media Info and Troubleshoot actions on episode cards', async () => {
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        return Promise.resolve(
          jsonResponse({ page: [{ artwork: '', collectionType: 'Episode', id: 91, mediaType: 'Episode', title: 'Pilot' }], totalCount: 1 })
        );
      }
      if (url === '/api/v1/collections' || url === '/api/v1/playlists/groups' || url === '/api/v1/schedules') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(jsonResponse(season));
    });

    render(<SeasonDetailScreen id={8} />);
    await waitFor(() => expect(screen.getByText('Pilot')).toBeInTheDocument());
    expect(screen.getByRole('button', { name: 'Media Info' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Troubleshoot Playback' })).toBeInTheDocument();
  });

  it('anchors and highlights the episode targeted by an #episode-{id} hash (#220)', async () => {
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        return Promise.resolve(
          jsonResponse({
            page: [
              { artwork: '', collectionType: 'Episode', id: 90, mediaType: 'Episode', title: 'Pilot' },
              { artwork: '', collectionType: 'Episode', id: 91, mediaType: 'Episode', title: 'Second' }
            ],
            totalCount: 2
          })
        );
      }
      if (url === '/api/v1/collections' || url === '/api/v1/playlists/groups' || url === '/api/v1/schedules') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(jsonResponse(season));
    });

    const scrollIntoView = vi.fn();
    vi.stubGlobal('HTMLElement', HTMLElement);
    Element.prototype.scrollIntoView = scrollIntoView;
    window.location.hash = '#episode-91';

    render(<SeasonDetailScreen id={8} />);
    await waitFor(() => expect(screen.getByText('Second')).toBeInTheDocument());

    const target = document.getElementById('episode-91');
    expect(target).not.toBeNull();
    await waitFor(() => expect(scrollIntoView).toHaveBeenCalled());
    expect(target?.className).toContain('ctv-media-card-highlighted');

    const other = document.getElementById('episode-90');
    expect(other?.className).not.toContain('ctv-media-card-highlighted');

    window.location.hash = '';
  });

  it('updates the anchor and scrolls on same-pathname in-app navigation (synthetic popstate, review #220)', async () => {
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        return Promise.resolve(
          jsonResponse({
            page: [
              { artwork: '', collectionType: 'Episode', id: 90, mediaType: 'Episode', title: 'Pilot' },
              { artwork: '', collectionType: 'Episode', id: 91, mediaType: 'Episode', title: 'Second' }
            ],
            totalCount: 2
          })
        );
      }
      if (url === '/api/v1/collections' || url === '/api/v1/playlists/groups' || url === '/api/v1/schedules') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(jsonResponse(season));
    });

    const scrollIntoView = vi.fn();
    Element.prototype.scrollIntoView = scrollIntoView;
    window.location.hash = '';

    render(<SeasonDetailScreen id={8} />);
    await waitFor(() => expect(screen.getByText('Second')).toBeInTheDocument());
    expect(scrollIntoView).not.toHaveBeenCalled();

    // `routing.ts`'s navigateToPath is what App.tsx uses for in-app clicks (e.g. an episode card
    // inside this same season grid): pushState + a synthetic `popstate`, not a real hash change,
    // so `hashchange` alone would never fire. This exercises that same-pathname path directly.
    navigateToPath('/app/media/seasons/8#episode-91');

    await waitFor(() => expect(scrollIntoView).toHaveBeenCalledTimes(1));
    const target = document.getElementById('episode-91');
    expect(target?.className).toContain('ctv-media-card-highlighted');

    window.location.hash = '';
  });

  it('does not re-scroll on refetch/pagination when the anchor is unchanged (review #220)', async () => {
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        const params = new URL(url, 'http://localhost').searchParams;
        const pageNum = params.get('pageNum');
        if (pageNum === '1') {
          return Promise.resolve(
            jsonResponse({
              page: [{ artwork: '', collectionType: 'Episode', id: 92, mediaType: 'Episode', title: 'Third' }],
              totalCount: 61
            })
          );
        }
        return Promise.resolve(
          jsonResponse({
            page: [
              { artwork: '', collectionType: 'Episode', id: 90, mediaType: 'Episode', title: 'Pilot' },
              { artwork: '', collectionType: 'Episode', id: 91, mediaType: 'Episode', title: 'Second' }
            ],
            totalCount: 61
          })
        );
      }
      if (url === '/api/v1/collections' || url === '/api/v1/playlists/groups' || url === '/api/v1/schedules') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(jsonResponse(season));
    });

    const scrollIntoView = vi.fn();
    Element.prototype.scrollIntoView = scrollIntoView;
    window.location.hash = '#episode-91';

    render(<SeasonDetailScreen id={8} />);
    await waitFor(() => expect(screen.getByText('Second')).toBeInTheDocument());
    await waitFor(() => expect(scrollIntoView).toHaveBeenCalledTimes(1));

    // Page away (target leaves the DOM) and back (a fresh `items` array is fetched, but the
    // anchor value itself never changed) — must not re-trigger the scroll.
    fireEvent.click(screen.getByTitle('Next page'));
    await waitFor(() => expect(screen.getByText('Third')).toBeInTheDocument());

    fireEvent.click(screen.getByTitle('Previous page'));
    await waitFor(() => expect(screen.getByText('Second')).toBeInTheDocument());

    expect(scrollIntoView).toHaveBeenCalledTimes(1);

    window.location.hash = '';
  });
});
