import json
from io import BytesIO
from json import JSONDecodeError
from unittest.mock import sentinel
from xml.etree import ElementTree

import pytest
from h_matchers import Any
from requests import Response

from tests.factories import TranscriptInfoFactory
from via.exceptions import UnhandledUpstreamException
from via.services.youtube_transcript import (
    TranscriptInfo,
    YouTubeTranscriptService,
    factory,
)


class TestTranscriptInfo:
    @pytest.mark.parametrize(
        "transcript_info,expected_id",
        [
            (
                TranscriptInfoFactory(),
                "en-us..RW5nbGlzaCAoVW5pdGVkIFN0YXRlcyk=",
            ),
            (
                TranscriptInfoFactory(autogenerated=True),
                "en-us.a.RW5nbGlzaCAoVW5pdGVkIFN0YXRlcyk=",
            ),
        ],
    )
    def test_id(self, transcript_info, expected_id):
        assert transcript_info.id == expected_id


class TestYouTubeTranscriptService:
    def test_get_transcript_infos(self, svc, http_service):
        # The JSON response body from the YouTube API.
        response_json = {
            "captions": {
                "playerCaptionsTracklistRenderer": {
                    "captionTracks": [
                        {
                            "languageCode": "en",
                            "name": {"simpleText": "English"},
                            "baseUrl": "https://example.com/transcript_1",
                        },
                        {
                            "languageCode": "en-us",
                            "name": {"simpleText": "English (United States)"},
                            "baseUrl": "https://example.com/transcript_2",
                        },
                    ]
                }
            }
        }
        response = http_service.post.return_value = Response()
        response.raw = BytesIO(json.dumps(response_json).encode("utf-8"))

        transcript_infos = svc.get_transcript_infos("test_video_id")

        http_service.post.assert_called_once_with(
            "https://youtubei.googleapis.com/youtubei/v1/player",
            json={
                "context": {
                    "client": {
                        "hl": "en",
                        "clientName": "WEB",
                        "clientVersion": "2.20210721.00.00",
                    }
                },
                "videoId": "test_video_id",
            },
        )
        caption_tracks = response_json["captions"]["playerCaptionsTracklistRenderer"][
            "captionTracks"
        ]
        assert transcript_infos == [
            Any.instance_of(TranscriptInfo).with_attrs(
                {
                    "language_code": caption_tracks[0]["languageCode"],
                    "autogenerated": False,
                    "name": caption_tracks[0]["name"]["simpleText"],
                    "url": caption_tracks[0]["baseUrl"],
                }
            ),
            Any.instance_of(TranscriptInfo).with_attrs(
                {
                    "language_code": caption_tracks[1]["languageCode"],
                    "autogenerated": False,
                    "name": caption_tracks[1]["name"]["simpleText"],
                    "url": caption_tracks[1]["baseUrl"],
                }
            ),
        ]

    def test_get_transcript_infos_error_response(self, svc, http_service):
        # We get an error response from the YouTube API.
        http_service.post.side_effect = UnhandledUpstreamException(
            "Something went wrong"
        )

        with pytest.raises(UnhandledUpstreamException):
            svc.get_transcript_infos("test_video_id")

    @pytest.mark.parametrize(
        "response_body,exception_class",
        [
            (b"foo", JSONDecodeError),  # Not valid JSON.
            (b"[]", TypeError),  # Not a dict.
            (b"{}", KeyError),  # No "captions" key.
            (b'{"captions": 23}', TypeError),  # "captions" isn't a dict.
            (b'{"captions": {}}', KeyError),  # No "playerCaptionsTracklistRenderer".
            # "playerCaptionsTracklistRenderer" isn't a dict.
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": 23}}',
                TypeError,
            ),
            # No "captionTracks".
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {}}}',
                KeyError,
            ),
            # "captionTracks" isn't a list.
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": 23}}}',
                TypeError,
            ),
            # No "languageCode".
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": [{"name": {"simpleText": "English"}, "baseUrl": "https://example.com"}]}}}',
                KeyError,
            ),
            # No "name".
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": [{"languageCode": "en", "baseUrl": "https://example.com"}]}}}',
                KeyError,
            ),
            # "name" isn't a dict.
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": [{"languageCode": "en", "name": 23, "baseUrl": "https://example.com"}]}}}',
                TypeError,
            ),
            # No "simpleText".
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": [{"languageCode": "en", "name": {}, "baseUrl": "https://example.com"}]}}}',
                KeyError,
            ),
            # No "baseUrl".
            (
                b'{"captions": {"playerCaptionsTracklistRenderer": {"captionTracks": [{"languageCode": "en", "name": {"simpleText": "English"}}]}}}',
                KeyError,
            ),
        ],
    )
    def test_get_transcript_infos_unexpected_response(
        self, svc, http_service, response_body, exception_class
    ):
        """It crashes if the response body isn't what we expect."""
        response = http_service.post.return_value = Response()
        response.encoding = "utf-8"
        response.raw = BytesIO(response_body)

        with pytest.raises(exception_class):
            svc.get_transcript_infos("test_video_id")

    @pytest.mark.parametrize(
        "transcript_infos,expected_default_transcript_index",
        [
            (
                [
                    TranscriptInfoFactory(language_code="en", name="English"),
                    TranscriptInfoFactory(
                        language_code="en-us", name="English (United States)"
                    ),
                ],
                0,
            ),
            (
                [
                    TranscriptInfoFactory(
                        language_code="en-us", name="English (United States)"
                    ),
                    TranscriptInfoFactory(language_code="en", name="English - DTVCC1"),
                ],
                0,
            ),
            (
                [
                    TranscriptInfoFactory(language_code="en", name="English - Foo"),
                    TranscriptInfoFactory(
                        language_code="en-us", name="English (United States) - Foo"
                    ),
                ],
                0,
            ),
            (
                [
                    TranscriptInfoFactory(
                        language_code="en-us", name="English (United States) - Foo"
                    ),
                    TranscriptInfoFactory(
                        language_code="en", name="English", autogenerated=True
                    ),
                ],
                0,
            ),
            (
                [
                    TranscriptInfoFactory(
                        language_code="en", name="English", autogenerated=True
                    ),
                    TranscriptInfoFactory(
                        language_code="en-us",
                        name="English (United States)",
                        autogenerated=True,
                    ),
                ],
                0,
            ),
            (
                [
                    TranscriptInfoFactory(language_code="fr", name="French"),
                    TranscriptInfoFactory(
                        language_code="en", name="English", autogenerated=True
                    ),
                ],
                1,
            ),
            (
                [
                    TranscriptInfoFactory(language_code="fr", name="French"),
                    TranscriptInfoFactory(language_code="de", name="Deutsch"),
                ],
                0,
            ),
        ],
    )
    def test_pick_default_transcript(
        self, svc, transcript_infos, expected_default_transcript_index
    ):
        assert (
            svc.pick_default_transcript(transcript_infos)
            == transcript_infos[expected_default_transcript_index]
        )

    def test_get_transcript(self, svc, transcript_info, http_service):
        http_service.get.return_value.text = """
            <transcript>
                <text start="0.21" dur="1.387">Hey there guys,</text>
                <text start="1.597">Lichen&#39; subscribe</text>
                <text start="4.327" dur="2.063">
                    &lt;font color=&quot;#A0AAB4&quot;&gt;Buy my merch!&lt;/font&gt;
                </text>
            </transcript>
        """

        transcript = svc.get_transcript(transcript_info)
        http_service.get.assert_called_once_with(transcript_info.url)

        assert transcript == [
            {"duration": 1.387, "start": 0.21, "text": "Hey there guys,"},
            {"duration": 0.0, "start": 1.597, "text": "Lichen' subscribe"},
            {"duration": 2.063, "start": 4.327, "text": "Buy my merch!"},
        ]

    def test_get_transcript_error_response(self, svc, transcript_info, http_service):
        # We get an error response from the YouTube API.
        http_service.get.side_effect = UnhandledUpstreamException(
            "Something went wrong"
        )

        with pytest.raises(UnhandledUpstreamException):
            svc.get_transcript(transcript_info)

    def test_get_transcript_unexpected_response(
        self, svc, transcript_info, http_service
    ):
        http_service.get.return_value.text = "foo"

        with pytest.raises(ElementTree.ParseError):
            svc.get_transcript(transcript_info)

    @pytest.fixture
    def svc(self, http_service):
        return YouTubeTranscriptService(http_service)


class TestFactory:
    def test_factory(self, YouTubeTranscriptService, http_service, pyramid_request):
        svc = factory(sentinel.context, pyramid_request)

        YouTubeTranscriptService.assert_called_once_with(http_service=http_service)
        assert svc == YouTubeTranscriptService.return_value

    @pytest.fixture
    def YouTubeTranscriptService(self, patch):
        return patch("via.services.youtube_transcript.YouTubeTranscriptService")
