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

import { StoryContentArea } from '@/Components/StoryContentArea';
import { useStoryAudio } from '@/hooks/useStoryAudio';
import type { ReaderStory, StoryParagraph } from '@/types/story-reader';

// The gate under test has nothing to do with the fade animation, and
// AnimatePresence's `mode="wait"` would hold the outgoing paragraph on screen
// while we assert on the incoming one.
vi.mock('@/Components/Transition/FadePresence', () => ({
  default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

const START_QUIZ_COLORS = {
  background: '#000',
  hover: '#000',
  active: '#000',
  disabled: '#000',
  text: '#fff',
  textHover: '#fff',
  textActive: '#fff',
  textDisabled: '#fff',
};

function buildParagraphs(): StoryParagraph[] {
  return [1, 2].map((paragraphNumber) => ({
    id: paragraphNumber,
    paragraph_number: paragraphNumber,
    content: `Paragraph ${paragraphNumber}.`,
    sentences: [1, 2].map((sentenceNumber) => ({
      id: paragraphNumber * 10 + sentenceNumber,
      sentence_number: sentenceNumber,
      content: `Sentence p${paragraphNumber}s${sentenceNumber}.`,
      audio_url: `/storage/audio/v1-p${paragraphNumber}-s${sentenceNumber}.mp4`,
    })),
  }));
}

function buildSingleParagraph(): StoryParagraph[] {
  return [
    {
      id: 1,
      paragraph_number: 1,
      content: 'Paragraph 1.',
      sentences: [1, 2].map((sentenceNumber) => ({
        id: sentenceNumber,
        sentence_number: sentenceNumber,
        content: `Sentence p1s${sentenceNumber}.`,
        audio_url: `/storage/audio/v1-p1-s${sentenceNumber}.mp4`,
      })),
    },
  ];
}

const story: ReaderStory = {
  id: 1,
  slug: 'a-stubborn-kitty',
  title: 'A Stubborn Kitty',
  synopsis: '',
  media: [],
  color_theme: 'default',
};

/**
 * Mirrors how Story.tsx wires the hook to the reader: the hook owns playback,
 * StoryContentArea owns the sentence clicks, and `canStartQuiz` is derived from
 * `hasStoryCompleted`. Teacher/admin bypass and prior attempts are left out so
 * the test only exercises the first-read gate.
 */
function StoryReaderHarness({ versions }: { versions: StoryParagraph[][] }) {
  const [currentParagraph, setCurrentParagraph] = useState(0);
  const [isQuizMode, setIsQuizMode] = useState(false);
  const [versionIndex, setVersionIndex] = useState(0);
  const paragraphs = versions[versionIndex] ?? [];
  const currentVersion = versionIndex + 1;

  const {
    hasStoryCompleted,
    currentlyPlayingSentence,
    togglePlayPause,
    handleParagraphNavSelect,
    handleSliderStart,
    handleProgressChange,
    handleSliderEnd,
    sentencePlayback,
  } = useStoryAudio({
    story,
    paragraphs,
    currentVersionData: { id: currentVersion, version_number: currentVersion, pages: [] },
    currentVersion,
    totalParagraphs: paragraphs.length,
    currentParagraph,
    setCurrentParagraph,
    setIsQuizMode,
  });

  const paragraph = paragraphs[currentParagraph];

  return (
    <div>
      <span data-testid="can-start-quiz">{String(hasStoryCompleted)}</span>
      <span data-testid="is-quiz-mode">{String(isQuizMode)}</span>
      <button onClick={togglePlayPause}>toggle-play</button>
      <button onClick={() => handleParagraphNavSelect(currentParagraph + 1)}>next-paragraph</button>
      <button
        onClick={() => {
          handleSliderStart();
          handleProgressChange([100]);
          handleSliderEnd();
        }}
      >
        scrub-to-end
      </button>
      <button onClick={() => setIsQuizMode(true)}>show-quiz-intro</button>
      <button
        onClick={() => {
          setCurrentParagraph(0);
          setVersionIndex((index) => index + 1);
        }}
      >
        next-version
      </button>
      <StoryContentArea
        isQuizMode={isQuizMode}
        canStartQuiz={hasStoryCompleted}
        hasQuizProgress={false}
        paragraphData={{
          content: paragraph?.content ?? '',
          sentences: (paragraph?.sentences ?? []).map((sentence) => sentence.content),
          isSingleSentence: false,
          maxLines: 4,
          backgroundImage: null,
          textWidth: null,
          textOffsetX: null,
        }}
        currentParagraph={currentParagraph}
        currentVersion={currentVersion}
        currentlyPlayingSentence={currentlyPlayingSentence}
        startQuizColors={START_QUIZ_COLORS}
        onStartQuiz={() => {}}
        sentencePlayback={sentencePlayback}
        quizPrefacePart1={null}
        quizPrefacePart2={null}
      />
    </div>
  );
}

let lastPlayed: HTMLMediaElement | null = null;

function rememberPlayedElement(element: HTMLMediaElement) {
  lastPlayed = element;
}

beforeEach(() => {
  vi.useFakeTimers({ shouldAdvanceTime: true });
  // jsdom implements neither play() nor pause(); capturing `this` also tells us
  // which element the hook is actually driving, so we can end its clip.
  vi.spyOn(HTMLMediaElement.prototype, 'play').mockImplementation(function (
    this: HTMLMediaElement
  ) {
    rememberPlayedElement(this);
    return Promise.resolve();
  });
  vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
});

afterEach(() => {
  // Unmount before the media stubs go away, or the hook's teardown reaches
  // jsdom's unimplemented pause() and prints a stack trace.
  cleanup();
  lastPlayed = null;
  vi.useRealTimers();
  vi.restoreAllMocks();
});

function finishCurrentClip() {
  act(() => {
    lastPlayed?.dispatchEvent(new Event('ended'));
  });
  // The queue schedules the next clip behind a short gap/paragraph delay.
  act(() => {
    vi.advanceTimersByTime(600);
  });
}

function clickSentence(text: string) {
  act(() => {
    fireEvent.click(screen.getByText(text, { exact: false }));
  });
}

function canStartQuiz() {
  return screen.getByTestId('can-start-quiz').textContent;
}

function isQuizMode() {
  return screen.getByTestId('is-quiz-mode').textContent;
}

describe('the first-read gate on the Start Quiz button', () => {
  it('unlocks once the student has played every sentence individually', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    clickSentence('Sentence p1s1.');
    finishCurrentClip();
    clickSentence('Sentence p1s2.');
    finishCurrentClip();

    act(() => {
      fireEvent.click(screen.getByText('next-paragraph'));
    });

    clickSentence('Sentence p2s1.');
    finishCurrentClip();
    clickSentence('Sentence p2s2.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('true');
  });

  it('unlocks when playback is paused partway and the rest is clicked sentence by sentence', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    act(() => {
      fireEvent.click(screen.getByText('toggle-play'));
    });
    finishCurrentClip(); // p1s1 played by the audio reading, p1s2 now playing

    act(() => {
      fireEvent.click(screen.getByText('toggle-play')); // student pauses
    });

    clickSentence('Sentence p1s2.');
    finishCurrentClip();

    act(() => {
      fireEvent.click(screen.getByText('next-paragraph'));
    });

    clickSentence('Sentence p2s1.');
    finishCurrentClip();
    clickSentence('Sentence p2s2.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('true');
  });

  it('unlocks after an uninterrupted play-through', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    act(() => {
      fireEvent.click(screen.getByText('toggle-play'));
    });
    finishCurrentClip();
    finishCurrentClip();
    finishCurrentClip();
    finishCurrentClip();

    expect(canStartQuiz()).toBe('true');
  });

  it('stays locked when the student scrubs to the end without playing anything', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    act(() => {
      fireEvent.click(screen.getByText('scrub-to-end'));
    });

    expect(canStartQuiz()).toBe('false');
  });

  it('relocks when the student switches to a readability version they have not read', () => {
    const easierVersion: StoryParagraph[] = [
      {
        id: 3,
        paragraph_number: 1,
        content: 'Easier paragraph.',
        sentences: [1, 2].map((sentenceNumber) => ({
          id: 30 + sentenceNumber,
          sentence_number: sentenceNumber,
          content: `Easier sentence s${sentenceNumber}.`,
          audio_url: `/storage/audio/v2-p1-s${sentenceNumber}.mp4`,
        })),
      },
    ];

    render(<StoryReaderHarness versions={[buildParagraphs(), easierVersion]} />);

    clickSentence('Sentence p1s1.');
    finishCurrentClip();
    clickSentence('Sentence p1s2.');
    finishCurrentClip();

    act(() => {
      fireEvent.click(screen.getByText('next-paragraph'));
    });

    clickSentence('Sentence p2s1.');
    finishCurrentClip();
    clickSentence('Sentence p2s2.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('true');

    act(() => {
      fireEvent.click(screen.getByText('next-version'));
    });

    expect(canStartQuiz()).toBe('false');
  });

  it('stays locked when a sentence is interrupted before it finishes playing', () => {
    render(<StoryReaderHarness versions={[buildSingleParagraph()]} />);

    clickSentence('Sentence p1s1.');
    clickSentence('Sentence p1s2.'); // cuts the first sentence off partway
    finishCurrentClip();

    expect(canStartQuiz()).toBe('false');
  });

  it('still credits a clicked sentence that was paused and then resumed', () => {
    render(<StoryReaderHarness versions={[buildSingleParagraph()]} />);

    clickSentence('Sentence p1s1.');
    act(() => {
      fireEvent.click(screen.getByText('toggle-play')); // pause mid-sentence
    });
    act(() => {
      fireEvent.click(screen.getByText('toggle-play')); // resume the same sentence
    });
    finishCurrentClip();

    clickSentence('Sentence p1s2.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('true');
  });

  it('stays locked when a sentence is interrupted by scrubbing the timeline', () => {
    render(<StoryReaderHarness versions={[buildSingleParagraph()]} />);

    clickSentence('Sentence p1s1.');
    act(() => {
      fireEvent.click(screen.getByText('scrub-to-end')); // cuts the first sentence off partway
    });
    finishCurrentClip();

    clickSentence('Sentence p1s2.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('false');
  });

  it('stays locked when only some sentences have been played', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    clickSentence('Sentence p1s1.');
    finishCurrentClip();

    expect(canStartQuiz()).toBe('false');
  });
});

describe('pressing Play after individually playing the last sentence (ENG-2130)', () => {
  it('advances to the quiz when the last sentence of the last paragraph was clicked and played', () => {
    render(<StoryReaderHarness versions={[buildSingleParagraph()]} />);

    // Click the last sentence to play it on its own, and let it finish.
    clickSentence('Sentence p1s2.');
    finishCurrentClip();
    expect(isQuizMode()).toBe('false');

    // With no more sentences to play, pressing Play should move to the quiz page
    // rather than animating forever with nothing queued.
    act(() => {
      fireEvent.click(screen.getByText('toggle-play'));
    });

    expect(isQuizMode()).toBe('true');
  });

  it('does not advance to the quiz when a non-last sentence was clicked; it plays on', () => {
    render(<StoryReaderHarness versions={[buildParagraphs()]} />);

    // Click the first sentence individually and let it finish.
    clickSentence('Sentence p1s1.');
    finishCurrentClip();

    // Pressing Play resumes the sequence from the next sentence, not the quiz.
    act(() => {
      fireEvent.click(screen.getByText('toggle-play'));
    });

    expect(isQuizMode()).toBe('false');
  });

  it('still restarts the story from the beginning when Play is pressed on the quiz page', () => {
    render(<StoryReaderHarness versions={[buildSingleParagraph()]} />);

    clickSentence('Sentence p1s2.');
    finishCurrentClip();
    act(() => {
      fireEvent.click(screen.getByText('toggle-play')); // enters the quiz page
    });
    expect(isQuizMode()).toBe('true');

    // Pressing Play on the quiz page restarts from the top (existing behaviour).
    act(() => {
      fireEvent.click(screen.getByText('toggle-play'));
    });

    expect(isQuizMode()).toBe('false');
  });
});
