# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import sys
import curses
import subprocess
from collections import OrderedDict

import pytest

from rtv.submission_page import SubmissionPage
from rtv.docs import FOOTER_SUBMISSION

try:
    from unittest import mock
except ImportError:
    import mock


PROMPTS = OrderedDict([
    ('prompt_1', 'comments/571dw3'),
    ('prompt_2', '///comments/571dw3'),
    ('prompt_3', '/comments/571dw3'),
    ('prompt_4', '/r/pics/comments/571dw3/'),
    ('prompt_5', 'https://www.reddit.com/r/pics/comments/571dw3/at_disneyland'),
])


def test_submission_page_construct(reddit, terminal, config, oauth):
    window = terminal.stdscr.subwin
    url = ('https://www.reddit.com/r/Python/comments/2xmo63/'
           'a_python_terminal_viewer_for_browsing_reddit')

    with terminal.loader():
        page = SubmissionPage(reddit, terminal, config, oauth, url=url)
    assert terminal.loader.exception is None

    # Toggle the second comment so we can check the draw more comments method
    page.content.toggle(1)

    # Set some special flags to make sure that we can draw them
    submission_data = page.content.get(-1)
    submission_data['gold'] = True
    submission_data['stickied'] = True
    submission_data['saved'] = True
    submission_data['flair'] = 'flair'

    # Set some special flags to make sure that we can draw them
    comment_data = page.content.get(0)
    comment_data['gold'] = True
    comment_data['stickied'] = True
    comment_data['saved'] = True
    comment_data['flair'] = 'flair'

    page.draw()

    #  Title
    title = url[:terminal.stdscr.ncols-1].encode('utf-8')
    window.addstr.assert_any_call(0, 0, title)

    # Banner
    menu = '[1]hot         [2]top         [3]rising         [4]new         [5]controversial'
    window.addstr.assert_any_call(0, 0, menu.encode('utf-8'))

    # Footer - The text is longer than the default terminal width
    text = FOOTER_SUBMISSION.strip()[:79]
    window.addstr.assert_any_call(0, 0, text.encode('utf-8'))

    # Submission
    submission_data = page.content.get(-1)
    text = submission_data['title'].encode('utf-8')
    window.subwin.addstr.assert_any_call(1, 1, text, 2097152)
    assert window.subwin.border.called

    # Comment
    comment_data = page.content.get(0)
    text = comment_data['split_body'][0].encode('utf-8')
    window.subwin.addstr.assert_any_call(1, 1, text, curses.A_NORMAL)

    # More Comments
    comment_data = page.content.get(1)
    text = comment_data['body'].encode('utf-8')
    window.subwin.addstr.assert_any_call(0, 1, text, curses.A_NORMAL)

    # Cursor should not be drawn when the page is first opened
    assert not any(args[0][3] == curses.A_REVERSE
                   for args in window.subwin.addch.call_args_list)

    # Reload with a smaller terminal window
    terminal.stdscr.ncols = 20
    terminal.stdscr.nlines = 10
    with terminal.loader():
        page = SubmissionPage(reddit, terminal, config, oauth, url=url)
    assert terminal.loader.exception is None
    page.draw()


def test_submission_refresh(submission_page):

    # Should be able to refresh content
    submission_page.refresh_content()


def test_submission_exit(submission_page):

    # Exiting should set active to false
    submission_page.active = True
    submission_page.controller.trigger('h')
    assert not submission_page.active


def test_submission_unauthenticated(submission_page, terminal):

    # Unauthenticated commands
    methods = [
        'a',  # Upvote
        'z',  # Downvote
        'c',  # Comment
        'e',  # Edit
        'd',  # Delete
        'w',  # Save
    ]
    for ch in methods:
        submission_page.controller.trigger(ch)
        text = 'Not logged in'.encode('utf-8')
        terminal.stdscr.subwin.addstr.assert_called_with(1, 1, text)


def test_submission_open(submission_page, terminal):

    # Open the selected link with the web browser
    with mock.patch.object(terminal, 'open_browser'):
        submission_page.controller.trigger(terminal.RETURN)
        assert terminal.open_browser.called


def test_submission_prompt(submission_page, terminal):

    # Prompt for a different subreddit
    with mock.patch.object(terminal, 'prompt_input'):
        # Valid input
        submission_page.active = True
        submission_page.selected_subreddit = None
        terminal.prompt_input.return_value = 'front/top'
        submission_page.controller.trigger('/')
        assert not submission_page.active
        assert submission_page.selected_subreddit

        # Invalid input
        submission_page.active = True
        submission_page.selected_subreddit = None
        terminal.prompt_input.return_value = 'front/pot'
        submission_page.controller.trigger('/')
        assert submission_page.active
        assert not submission_page.selected_subreddit


@pytest.mark.parametrize('prompt', PROMPTS.values(), ids=list(PROMPTS))
def test_submission_prompt_submission(submission_page, terminal, prompt):

    # Navigate to a different submission from inside a submission
    with mock.patch.object(terminal, 'prompt_input'):
        terminal.prompt_input.return_value = prompt
        submission_page.content.order = 'top'
        submission_page.controller.trigger('/')
        assert not terminal.loader.exception
        data = submission_page.content.get(-1)
        assert data['object'].id == '571dw3'
        assert submission_page.content.order is None


def test_submission_order(submission_page):

    submission_page.controller.trigger('1')
    assert submission_page.content.order == 'hot'
    submission_page.controller.trigger('2')
    assert submission_page.content.order == 'top'
    submission_page.controller.trigger('3')
    assert submission_page.content.order == 'rising'
    submission_page.controller.trigger('4')
    assert submission_page.content.order == 'new'
    submission_page.controller.trigger('5')
    assert submission_page.content.order == 'controversial'

    # Shouldn't be able to sort the submission page by gilded
    submission_page.controller.trigger('6')
    assert submission_page.content.order == 'controversial'


def test_submission_move_top_bottom(submission_page):

    submission_page.controller.trigger('G')
    assert submission_page.nav.absolute_index == 44

    submission_page.controller.trigger('g')
    submission_page.controller.trigger('g')
    assert submission_page.nav.absolute_index == -1


def test_submission_move_sibling_parent(submission_page):

    # Jump to sibling
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('j')
        submission_page.controller.trigger('J')
    assert submission_page.nav.absolute_index == 7

    # Jump to parent
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('k')
        submission_page.controller.trigger('k')
        submission_page.controller.trigger('K')
    assert submission_page.nav.absolute_index == 0


def test_submission_pager(submission_page, terminal):

    # View a submission with the pager
    with mock.patch.object(terminal, 'open_pager'):
        submission_page.controller.trigger('l')
        assert terminal.open_pager.called

    # Move down to the first comment
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('j')

    # View a comment with the pager
    with mock.patch.object(terminal, 'open_pager'):
        submission_page.controller.trigger('l')
        assert terminal.open_pager.called


def test_submission_comment_not_enough_space(submission_page, terminal):

    # The first comment is 10 lines, shrink the screen so that it won't fit.
    # Setting the terminal to 10 lines means that there will only be 8 lines
    # available (after subtracting the header and footer) to draw the comment.
    terminal.stdscr.nlines = 10

    # Select the first comment
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.move_cursor_down()

    submission_page.draw()

    text = '(Not enough space to display)'.encode('ascii')
    window = terminal.stdscr.subwin
    window.subwin.addstr.assert_any_call(6, 1, text, curses.A_NORMAL)


def test_submission_vote(submission_page, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Test voting on the submission
    with mock.patch('rtv.packages.praw.objects.Submission.upvote') as upvote,            \
            mock.patch('rtv.packages.praw.objects.Submission.downvote') as downvote,     \
            mock.patch('rtv.packages.praw.objects.Submission.clear_vote') as clear_vote:

        data = submission_page.get_selected_item()
        data['object'].archived = False

        # Upvote
        submission_page.controller.trigger('a')
        assert upvote.called
        assert data['likes'] is True

        # Clear vote
        submission_page.controller.trigger('a')
        assert clear_vote.called
        assert data['likes'] is None

        # Upvote
        submission_page.controller.trigger('a')
        assert upvote.called
        assert data['likes'] is True

        # Downvote
        submission_page.controller.trigger('z')
        assert downvote.called
        assert data['likes'] is False

        # Clear vote
        submission_page.controller.trigger('z')
        assert clear_vote.called
        assert data['likes'] is None

        # Upvote - exception
        upvote.side_effect = KeyboardInterrupt
        submission_page.controller.trigger('a')
        assert data['likes'] is None

        # Downvote - exception
        downvote.side_effect = KeyboardInterrupt
        submission_page.controller.trigger('a')
        assert data['likes'] is None


def test_submission_vote_archived(submission_page, refresh_token, terminal):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Load an archived submission
    archived_url = 'https://www.reddit.com/r/IAmA/comments/z1c9z/'
    submission_page.refresh_content(name=archived_url)

    with mock.patch.object(terminal, 'show_notification') as show_notification:
        data = submission_page.get_selected_item()

        # Upvote the submission
        show_notification.reset_mock()
        submission_page.controller.trigger('a')
        show_notification.assert_called_with('Voting disabled for archived post', style='Error')
        assert data['likes'] is None

        # Downvote the submission
        show_notification.reset_mock()
        submission_page.controller.trigger('z')
        show_notification.assert_called_with('Voting disabled for archived post', style='Error')
        assert data['likes'] is None


def test_submission_save(submission_page, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Test save on the submission
    with mock.patch('rtv.packages.praw.objects.Submission.save') as save,        \
            mock.patch('rtv.packages.praw.objects.Submission.unsave') as unsave:

        data = submission_page.content.get(submission_page.nav.absolute_index)

        # Save
        submission_page.controller.trigger('w')
        assert save.called
        assert data['saved'] is True

        # Unsave
        submission_page.controller.trigger('w')
        assert unsave.called
        assert data['saved'] is False

        # Save - exception
        save.side_effect = KeyboardInterrupt
        submission_page.controller.trigger('w')
        assert data['saved'] is False


def test_submission_comment_save(submission_page, terminal, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Move down to the first comment
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('j')

    # Test save on the comment submission
    with mock.patch('rtv.packages.praw.objects.Comment.save') as save,        \
            mock.patch('rtv.packages.praw.objects.Comment.unsave') as unsave:

        data = submission_page.content.get(submission_page.nav.absolute_index)

        # Save
        submission_page.controller.trigger('w')
        assert save.called
        assert data['saved'] is True

        # Unsave
        submission_page.controller.trigger('w')
        assert unsave.called
        assert data['saved'] is False

        # Save - exception
        save.side_effect = KeyboardInterrupt
        submission_page.controller.trigger('w')
        assert data['saved'] is False


def test_submission_comment(submission_page, terminal, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Leave a comment
    with mock.patch('rtv.packages.praw.objects.Submission.add_comment') as add_comment, \
            mock.patch.object(terminal, 'open_editor') as open_editor,                  \
            mock.patch('time.sleep'):
        open_editor.return_value.__enter__.return_value = 'comment text'
        submission_page.controller.trigger('c')
        assert open_editor.called
        add_comment.assert_called_with('comment text')


def test_submission_delete(submission_page, terminal, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Can't delete the submission
    curses.flash.reset_mock()
    submission_page.controller.trigger('d')
    assert curses.flash.called

    # Move down to the first comment
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('j')

    # Try to delete the first comment - wrong author
    curses.flash.reset_mock()
    submission_page.controller.trigger('d')
    assert curses.flash.called

    # Spoof the author and try to delete again
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['author'] = submission_page.reddit.user.name
    with mock.patch('rtv.packages.praw.objects.Comment.delete') as delete,  \
            mock.patch.object(terminal.stdscr, 'getch') as getch,           \
            mock.patch('time.sleep'):
        getch.return_value = ord('y')
        submission_page.controller.trigger('d')
        assert delete.called


def test_submission_edit(submission_page, terminal, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Try to edit the submission - wrong author
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['author'] = 'some other person'
    curses.flash.reset_mock()
    submission_page.controller.trigger('e')
    assert curses.flash.called

    # Spoof the submission and try to edit again
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['author'] = submission_page.reddit.user.name
    with mock.patch('rtv.packages.praw.objects.Submission.edit') as edit,  \
            mock.patch.object(terminal, 'open_editor') as open_editor,     \
            mock.patch('time.sleep'):
        open_editor.return_value.__enter__.return_value = 'submission text'

        submission_page.controller.trigger('e')
        assert open_editor.called
        edit.assert_called_with('submission text')

    # Move down to the first comment
    with mock.patch.object(submission_page, 'clear_input_queue'):
        submission_page.controller.trigger('j')

    # Spoof the author and edit the comment
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['author'] = submission_page.reddit.user.name
    with mock.patch('rtv.packages.praw.objects.Comment.edit') as edit, \
            mock.patch.object(terminal, 'open_editor') as open_editor, \
            mock.patch('time.sleep'):
        open_editor.return_value.__enter__.return_value = 'comment text'

        submission_page.controller.trigger('e')
        assert open_editor.called
        edit.assert_called_with('comment text')


def test_submission_urlview(submission_page, terminal, refresh_token):

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Submission case
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['body'] = 'test comment body  ❤'
    with mock.patch.object(terminal, 'open_urlview') as open_urlview:
        submission_page.controller.trigger('b')
        open_urlview.assert_called_with('test comment body  ❤')

    # Subreddit case
    data = submission_page.content.get(submission_page.nav.absolute_index)
    data['text'] = ''
    data['body'] = ''
    data['url_full'] = 'http://test.url.com  ❤'
    with mock.patch.object(terminal, 'open_urlview') as open_urlview, \
            mock.patch('subprocess.Popen'):
        submission_page.controller.trigger('b')
        open_urlview.assert_called_with('http://test.url.com  ❤')


@pytest.mark.skipif(sys.platform != 'darwin', reason='Test uses osx clipboard')
def test_copy_to_clipboard_osx(submission_page, terminal, refresh_token):

    def get_clipboard_content():
        p = subprocess.Popen(['pbpaste', 'r'],
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             close_fds=True)
        stdout, stderr = p.communicate()
        return stdout.decode('utf-8')

    window = terminal.stdscr.subwin

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Get submission
    data = submission_page.content.get(submission_page.nav.absolute_index)

    # Trigger copy command for permalink
    submission_page.controller.trigger('y')
    assert data.get('permalink') == get_clipboard_content()
    window.addstr.assert_called_with(1, 1, b'Copied permalink to clipboard')

    # Trigger copy command for submission
    submission_page.controller.trigger('Y')
    assert data.get('url_full') == get_clipboard_content()
    window.addstr.assert_called_with(1, 1, b'Copied url to clipboard')


@pytest.mark.skipif(sys.platform == 'darwin', reason='Test uses linux clipboard')
def test_copy_to_clipboard_linux(submission_page, terminal, refresh_token):

    def get_clipboard_content():
        paste_cmd = None
        for cmd in ['xsel', 'xclip']:
            cmd_exists = subprocess.call(
                ['which', cmd],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
            if cmd_exists:
                paste_cmd = cmd
                break

        if paste_cmd is not None:
            cmd_args = {'xsel': ['xsel', '-b', '-o'],
                        'xclip': ['xclip', '-selection', 'c', '-o']}
            p = subprocess.Popen(cmd_args.get(paste_cmd),
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE,
                                 close_fds=True)
            stdout, stderr = p.communicate()
            return stdout.decode('utf-8')

    window = terminal.stdscr.subwin

    # Log in
    submission_page.config.refresh_token = refresh_token
    submission_page.oauth.authorize()

    # Get submission
    data = submission_page.content.get(submission_page.nav.absolute_index)

    # Trigger copy command for permalink
    submission_page.controller.trigger('y')
    content = get_clipboard_content()
    if content is not None:
        assert data.get('permalink') == content
        window.addstr.assert_called_with(1, 1, b'Copied permalink to clipboard')
    else:
        # Neither xclip or xsel installed, this is what happens on Travis CI
        text = b'Failed to copy permalink: External copy application not found'
        window.addstr.assert_called_with(1, 1, text)

    # Trigger copy command for url
    submission_page.controller.trigger('Y')
    content = get_clipboard_content()
    if content is not None:
        assert data.get('url_full') == content
        window.addstr.assert_called_with(1, 1, b'Copied url to clipboard')
    else:
        # Neither xclip or xsel installed, this is what happens on Travis CI
        text = b'Failed to copy url: External copy application not found'
        window.addstr.assert_called_with(1, 1, text)
