import asyncio
import configparser
import types
from unittest import mock
from unittest.mock import MagicMock

import pytest
import requests
from prawcore import Requestor
from prawcore.exceptions import BadRequest

from praw import Reddit, __version__
from praw.config import Config
from praw.exceptions import ClientException, RedditAPIException

from . import UnitTest


class TestReddit(UnitTest):
    REQUIRED_DUMMY_SETTINGS = dict.fromkeys(["client_id", "client_secret", "user_agent"], "dummy")

    @staticmethod
    async def check_async(reddit):
        reddit.request(method="GET", path="path")

    @staticmethod
    def patch_request(*args, **kwargs):
        """Patch requests to return mock data on specific url."""
        response = requests.Response()
        response._content = b'{"name":"username"}'
        response.status_code = 200
        return response

    def test_check_for_async(self, caplog):
        reddit = Reddit(**self.REQUIRED_DUMMY_SETTINGS)
        reddit._core.request = self.patch_request
        asyncio.run(self.check_async(reddit))
        log_record = caplog.records[0]
        assert log_record.levelname == "WARNING"
        assert (
            log_record.message == "It appears that you are using PRAW in an asynchronous environment.\nIt"
            " is strongly recommended to use Async PRAW:"
            " https://asyncpraw.readthedocs.io.\nSee"
            " https://praw.readthedocs.io/en/latest/getting_started"
            "/multiple_instances.html#discord-bots-and-asynchronous-environments"
            " for more info.\n"
        )

    def test_check_for_async__disabled(self, caplog):
        reddit = Reddit(check_for_async=False, **self.REQUIRED_DUMMY_SETTINGS)
        reddit._core.request = self.patch_request
        asyncio.run(self.check_async(reddit))
        assert caplog.records == []

    @mock.patch("praw.reddit.UPDATE_CHECKER_MISSING", False)
    @mock.patch("praw.reddit.Reddit.update_checked", False)
    @mock.patch("praw.reddit.update_check", create=True)
    def test_check_for_updates(self, mock_update_check):
        Reddit(check_for_updates="1", **self.REQUIRED_DUMMY_SETTINGS)
        assert Reddit.update_checked
        mock_update_check.assert_called_with(package_name="praw", package_version=__version__)

    @mock.patch("praw.reddit.UPDATE_CHECKER_MISSING", True)
    @mock.patch("praw.reddit.Reddit.update_checked", False)
    @mock.patch("praw.reddit.update_check", create=True)
    def test_check_for_updates_update_checker_missing(self, mock_update_check):
        Reddit(check_for_updates="1", **self.REQUIRED_DUMMY_SETTINGS)
        assert not Reddit.update_checked
        assert not mock_update_check.called

    def test_comment(self, reddit):
        assert reddit.comment("cklfmye").id == "cklfmye"

    def test_context_manager(self):
        with Reddit(**self.REQUIRED_DUMMY_SETTINGS) as reddit:
            assert not reddit.config.check_for_updates

    def test_info__invalid_param(self, reddit):
        with pytest.raises(TypeError) as excinfo:
            reddit.info(fullnames=None)

        err_str = "Either 'fullnames', 'url', or 'subreddits' must be provided."
        assert str(excinfo.value) == err_str

        with pytest.raises(TypeError) as excinfo:
            reddit.info(fullnames=[], url="")

        assert str(excinfo.value) == err_str

    def test_info__not_list(self, reddit):
        with pytest.raises(TypeError) as excinfo:
            reddit.info(fullnames="Let's try a string")

        assert "must be a non-str iterable" in str(excinfo.value)

    def test_invalid_config(self):
        with pytest.raises(ValueError) as excinfo:
            Reddit(timeout="test", **self.REQUIRED_DUMMY_SETTINGS)
        assert (
            excinfo.value.args[0] == "An incorrect config type was given for option timeout. The expected "
            "type is int, but the given value is test."
        )
        with pytest.raises(ValueError) as excinfo:
            Reddit(ratelimit_seconds="test", **self.REQUIRED_DUMMY_SETTINGS)
        assert (
            excinfo.value.args[0] == "An incorrect config type was given for option ratelimit_seconds. The "
            "expected type is int, but the given value is test."
        )

    def test_live_info__invalid_param(self, reddit):
        with pytest.raises(TypeError) as excinfo:
            reddit.live.info(None)
        assert str(excinfo.value) == "ids must be a list"

    def test_live_info__valid_param(self, reddit):
        gen = reddit.live.info(["dummy", "dummy2"])
        assert isinstance(gen, types.GeneratorType)

    def test_multireddit(self, reddit):
        assert reddit.multireddit(name="aa", redditor="bboe").path == "/user/bboe/m/aa"

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "Some unexpected error message",
                                "ratelimit",
                            ]
                        ]
                    }
                },
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__invalid_rate_limit_message(self, mock_sleep, reddit):
        with pytest.raises(RedditAPIException) as exception:
            reddit.post("test")
        assert exception.value.items[0].message == "Some unexpected error message"
        mock_sleep.assert_not_called()

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 1 minute.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
            ],
        ),
    )
    def test_post_ratelimit__over_threshold__minutes(self, reddit):
        with pytest.raises(RedditAPIException) as exception:
            reddit.post("test")
        assert exception.value.items[0].message == "You are doing that too much. Try again in 1 minute."

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 6 seconds.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__over_threshold__seconds(self, mock_sleep, reddit):
        with pytest.raises(RedditAPIException) as exception:
            reddit.post("test")
        assert exception.value.items[0].message == "You are doing that too much. Try again in 6 seconds."
        mock_sleep.assert_not_called()

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 2 milliseconds.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 1 millisecond.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {},
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__under_threshold__milliseconds(self, mock_sleep, reddit):
        reddit.post("test")
        mock_sleep.assert_has_calls([mock.call(1), mock.call(1)])

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 1 minute.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {},
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__under_threshold__minutes(self, mock_sleep, reddit):
        reddit.config.ratelimit_seconds = 60
        reddit.post("test")
        mock_sleep.assert_has_calls([mock.call(61)])

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 5 seconds.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {},
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__under_threshold__seconds(self, mock_sleep, reddit):
        reddit.post("test")
        mock_sleep.assert_has_calls([mock.call(6)])

    @mock.patch(
        "praw.Reddit.request",
        new=MagicMock(
            side_effect=[
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 5 seconds.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 3 seconds.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
                {
                    "json": {
                        "errors": [
                            [
                                "RATELIMIT",
                                "You are doing that too much. Try again in 1 second.",
                                "ratelimit",
                            ]
                        ]
                    }
                },
            ],
        ),
    )
    @mock.patch("time.sleep", return_value=None)
    def test_post_ratelimit__under_threshold__seconds_failure(self, mock_sleep, reddit):
        with pytest.raises(RedditAPIException) as exception:
            reddit.post("test")
        assert exception.value.items[0].message == "You are doing that too much. Try again in 1 second."
        mock_sleep.assert_has_calls([mock.call(6), mock.call(4), mock.call(2)])

    def test_read_only__with_authenticated_core__legacy_refresh_token(self):
        with Reddit(
            password=None,
            refresh_token="refresh",
            username=None,
            **self.REQUIRED_DUMMY_SETTINGS,
        ) as reddit:
            assert not reddit.read_only
            reddit.read_only = True
            assert reddit.read_only
            reddit.read_only = False
            assert not reddit.read_only

    def test_read_only__with_authenticated_core__non_confidential__legacy_refresh_token(
        self,
    ):
        with Reddit(
            client_id="dummy",
            client_secret=None,
            redirect_uri="dummy",
            refresh_token="dummy",
            user_agent="dummy",
        ) as reddit:
            assert not reddit.read_only
            reddit.read_only = True
            assert reddit.read_only
            reddit.read_only = False
            assert not reddit.read_only

    def test_read_only__with_script_authenticated_core(self):
        with Reddit(password="dummy", username="dummy", **self.REQUIRED_DUMMY_SETTINGS) as reddit:
            assert not reddit.read_only
            reddit.read_only = True
            assert reddit.read_only
            reddit.read_only = False
            assert not reddit.read_only

    def test_read_only__without_trusted_authenticated_core(self):
        with Reddit(password=None, username=None, **self.REQUIRED_DUMMY_SETTINGS) as reddit:
            assert reddit.read_only
            with pytest.raises(ClientException):
                reddit.read_only = False
            assert reddit.read_only
            reddit.read_only = True
            assert reddit.read_only

    def test_read_only__without_untrusted_authenticated_core(self):
        required_settings = self.REQUIRED_DUMMY_SETTINGS.copy()
        required_settings["client_secret"] = None
        with Reddit(password=None, username=None, **required_settings) as reddit:
            assert reddit.read_only
            with pytest.raises(ClientException):
                reddit.read_only = False
            assert reddit.read_only
            reddit.read_only = True
            assert reddit.read_only

    def test_reddit__missing_required_settings(self):
        for setting in self.REQUIRED_DUMMY_SETTINGS:
            with pytest.raises(ClientException) as excinfo:
                settings = self.REQUIRED_DUMMY_SETTINGS.copy()
                settings[setting] = Config.CONFIG_NOT_SET
                Reddit(**settings)
            assert str(excinfo.value).startswith(f"Required configuration setting '{setting}' missing.")
            if setting == "client_secret":
                assert "set to None" in str(excinfo.value)

    def test_reddit__required_settings_set_to_none(self):
        required_settings = self.REQUIRED_DUMMY_SETTINGS.copy()
        del required_settings["client_secret"]
        for setting in required_settings:
            with pytest.raises(ClientException) as excinfo:
                settings = self.REQUIRED_DUMMY_SETTINGS.copy()
                settings[setting] = None
                Reddit(**settings)
            assert str(excinfo.value).startswith(f"Required configuration setting '{setting}' missing.")

    def test_reddit__site_name_no_section(self):
        with pytest.raises(configparser.NoSectionError) as excinfo:
            Reddit("bad_site_name")
        assert "praw.readthedocs.io" in excinfo.value.message

    @mock.patch("prawcore.sessions.Session")
    def test_request__badrequest_with_no_json_body(self, mock_session):
        response = mock.Mock(status_code=400, text="")
        response.json.side_effect = ValueError
        mock_session.return_value.request = mock.Mock(side_effect=BadRequest(response))

        reddit = Reddit(client_id="dummy", client_secret="dummy", user_agent="dummy")
        with pytest.raises(Exception) as excinfo:
            reddit.request(method="POST", path="/")
        assert str(excinfo.value) == "received 400 HTTP response"

    def test_request__json_and_body(self):
        reddit = Reddit(client_id="dummy", client_secret="dummy", user_agent="dummy")
        with pytest.raises(ClientException) as excinfo:
            reddit.request(data={"key": "value"}, json={"key": "value"}, method="POST", path="/")
        assert str(excinfo.value).startswith("At most one of 'data' or 'json' is supported.")

    def test_submission(self, reddit):
        assert reddit.submission("2gmzqe").id == "2gmzqe"

    def test_subreddit(self, reddit):
        assert reddit.subreddit("redditdev").display_name == "redditdev"


class TestRedditCustomRequestor(UnitTest):
    def test_requestor_class(self, reddit):
        class CustomRequestor(Requestor):
            pass

        _reddit = Reddit(
            client_id="dummy",
            client_secret="dummy",
            password="dummy",
            requestor_class=CustomRequestor,
            user_agent="dummy",
            username="dummy",
        )
        assert isinstance(_reddit._core.requestor, CustomRequestor)
        assert not isinstance(reddit._core.requestor, CustomRequestor)

        _reddit = Reddit(
            client_id="dummy",
            client_secret="dummy",
            requestor_class=CustomRequestor,
            user_agent="dummy",
        )
        assert isinstance(_reddit._core.requestor, CustomRequestor)
        assert not isinstance(reddit._core.requestor, CustomRequestor)

    def test_requestor_kwargs(self):
        session = mock.Mock(headers={})
        reddit = Reddit(
            client_id="dummy",
            client_secret="dummy",
            requestor_kwargs={"session": session},
            user_agent="dummy",
        )

        assert reddit._core.requestor._http is session
