import argparse
import json
import os
from tempfile import gettempdir
from urllib.request import urlopen

import pytest

from httpie.cli.argtypes import (
    PARSED_DEFAULT_FORMAT_OPTIONS,
    parse_format_options,
)
from httpie.cli.definition import parser
from httpie.output.formatters.colors import get_lexer
from httpie.status import ExitStatus
from utils import COLOR, CRLF, HTTP_OK, MockEnvironment, http


@pytest.mark.parametrize('stdout_isatty', [True, False])
def test_output_option(httpbin, stdout_isatty):
    output_filename = os.path.join(gettempdir(), test_output_option.__name__)
    url = httpbin + '/robots.txt'

    r = http('--output', output_filename, url,
             env=MockEnvironment(stdout_isatty=stdout_isatty))
    assert r == ''

    expected_body = urlopen(url).read().decode()
    with open(output_filename, 'r') as f:
        actual_body = f.read()

    assert actual_body == expected_body


class TestVerboseFlag:
    def test_verbose(self, httpbin):
        r = http('--verbose',
                 'GET', httpbin.url + '/get', 'test-header:__test__')
        assert HTTP_OK in r
        assert r.count('__test__') == 2

    def test_verbose_form(self, httpbin):
        # https://github.com/jakubroztocil/httpie/issues/53
        r = http('--verbose', '--form', 'POST', httpbin.url + '/post',
                 'A=B', 'C=D')
        assert HTTP_OK in r
        assert 'A=B&C=D' in r

    def test_verbose_json(self, httpbin):
        r = http('--verbose',
                 'POST', httpbin.url + '/post', 'foo=bar', 'baz=bar')
        assert HTTP_OK in r
        assert '"baz": "bar"' in r

    def test_verbose_implies_all(self, httpbin):
        r = http('--verbose', '--follow', httpbin + '/redirect/1')
        assert 'GET /redirect/1 HTTP/1.1' in r
        assert 'HTTP/1.1 302 FOUND' in r
        assert 'GET /get HTTP/1.1' in r
        assert HTTP_OK in r


class TestColors:

    @pytest.mark.parametrize(
        argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
        argvalues=[
            ('application/json', False, None, 'JSON'),
            ('application/json+foo', False, None, 'JSON'),
            ('application/foo+json', False, None, 'JSON'),
            ('application/json-foo', False, None, 'JSON'),
            ('application/x-json', False, None, 'JSON'),
            ('foo/json', False, None, 'JSON'),
            ('foo/json+bar', False, None, 'JSON'),
            ('foo/bar+json', False, None, 'JSON'),
            ('foo/json-foo', False, None, 'JSON'),
            ('foo/x-json', False, None, 'JSON'),
            ('application/vnd.comverge.grid+hal+json', False, None, 'JSON'),
            ('text/plain', True, '{}', 'JSON'),
            ('text/plain', True, 'foo', 'Text only'),
        ]
    )
    def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
        lexer = get_lexer(mime, body=body, explicit_json=explicit_json)
        assert lexer is not None
        assert lexer.name == expected_lexer_name

    def test_get_lexer_not_found(self):
        assert get_lexer('xxx/yyy') is None


class TestPrettyOptions:
    """Test the --pretty handling."""

    def test_pretty_enabled_by_default(self, httpbin):
        env = MockEnvironment(colors=256)
        r = http('GET', httpbin.url + '/get', env=env)
        assert COLOR in r

    def test_pretty_enabled_by_default_unless_stdout_redirected(self, httpbin):
        r = http('GET', httpbin.url + '/get')
        assert COLOR not in r

    def test_force_pretty(self, httpbin):
        env = MockEnvironment(stdout_isatty=False, colors=256)
        r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, )
        assert COLOR in r

    def test_force_ugly(self, httpbin):
        r = http('--pretty=none', 'GET', httpbin.url + '/get')
        assert COLOR not in r

    def test_subtype_based_pygments_lexer_match(self, httpbin):
        """Test that media subtype is used if type/subtype doesn't
        match any lexer.

        """
        env = MockEnvironment(colors=256)
        r = http('--print=B', '--pretty=all', httpbin.url + '/post',
                 'Content-Type:text/foo+json', 'a=b', env=env)
        assert COLOR in r

    def test_colors_option(self, httpbin):
        env = MockEnvironment(colors=256)
        r = http('--print=B', '--pretty=colors',
                 'GET', httpbin.url + '/get', 'a=b',
                 env=env)
        # Tests that the JSON data isn't formatted.
        assert not r.strip().count('\n')
        assert COLOR in r

    def test_format_option(self, httpbin):
        env = MockEnvironment(colors=256)
        r = http('--print=B', '--pretty=format',
                 'GET', httpbin.url + '/get', 'a=b',
                 env=env)
        # Tests that the JSON data is formatted.
        assert r.strip().count('\n') == 2
        assert COLOR not in r


class TestLineEndings:
    """
    Test that CRLF is properly used in headers
    and as the headers/body separator.

    """

    def _validate_crlf(self, msg):
        lines = iter(msg.splitlines(True))
        for header in lines:
            if header == CRLF:
                break
            assert header.endswith(CRLF), repr(header)
        else:
            assert 0, 'CRLF between headers and body not found in %r' % msg
        body = ''.join(lines)
        assert CRLF not in body
        return body

    def test_CRLF_headers_only(self, httpbin):
        r = http('--headers', 'GET', httpbin.url + '/get')
        body = self._validate_crlf(r)
        assert not body, 'Garbage after headers: %r' % r

    def test_CRLF_ugly_response(self, httpbin):
        r = http('--pretty=none', 'GET', httpbin.url + '/get')
        self._validate_crlf(r)

    def test_CRLF_formatted_response(self, httpbin):
        r = http('--pretty=format', 'GET', httpbin.url + '/get')
        assert r.exit_status == ExitStatus.SUCCESS
        self._validate_crlf(r)

    def test_CRLF_ugly_request(self, httpbin):
        r = http('--pretty=none', '--print=HB', 'GET', httpbin.url + '/get')
        self._validate_crlf(r)

    def test_CRLF_formatted_request(self, httpbin):
        r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get')
        self._validate_crlf(r)


class TestFormatOptions:
    def test_header_formatting_options(self):
        def get_headers(sort):
            return http(
                '--offline', '--print=H',
                '--format-options', 'headers.sort:' + sort,
                'example.org', 'ZZZ:foo', 'XXX:foo',
            )

        r_sorted = get_headers('true')
        r_unsorted = get_headers('false')
        assert r_sorted != r_unsorted
        assert f'XXX: foo{CRLF}ZZZ: foo' in r_sorted
        assert f'ZZZ: foo{CRLF}XXX: foo' in r_unsorted

    @pytest.mark.parametrize(
        argnames=['options', 'expected_json'],
        argvalues=[
            # @formatter:off
            (
                'json.sort_keys:true,json.indent:4',
                json.dumps({'a': 0, 'b': 0}, indent=4),
            ),
            (
                'json.sort_keys:false,json.indent:2',
                json.dumps({'b': 0, 'a': 0}, indent=2),
            ),
            (
                'json.format:false',
                json.dumps({'b': 0, 'a': 0}),
            ),
            # @formatter:on
        ]
    )
    def test_json_formatting_options(self, options: str, expected_json: str):
        r = http(
            '--offline', '--print=B',
            '--format-options', options,
            'example.org', 'b:=0', 'a:=0',
        )
        assert expected_json in r

    @pytest.mark.parametrize(
        argnames=['defaults', 'options_string', 'expected'],
        argvalues=[
            # @formatter:off
            ({'foo': {'bar': 1}}, 'foo.bar:2', {'foo': {'bar': 2}}),
            ({'foo': {'bar': True}}, 'foo.bar:false', {'foo': {'bar': False}}),
            ({'foo': {'bar': 'a'}}, 'foo.bar:b', {'foo': {'bar': 'b'}}),
            # @formatter:on
        ]
    )
    def test_parse_format_options(self, defaults, options_string, expected):
        actual = parse_format_options(s=options_string, defaults=defaults)
        assert expected == actual

    @pytest.mark.parametrize(
        argnames=['options_string', 'expected_error'],
        argvalues=[
            ('foo:2', 'invalid option'),
            ('foo.baz:2', 'invalid key'),
            ('foo.bar:false', 'expected int got bool'),
        ]
    )
    def test_parse_format_options_errors(self, options_string, expected_error):
        defaults = {
            'foo': {
                'bar': 1
            }
        }
        with pytest.raises(argparse.ArgumentTypeError, match=expected_error):
            parse_format_options(s=options_string, defaults=defaults)

    @pytest.mark.parametrize(
        argnames=['args', 'expected_format_options'],
        argvalues=[
            (
                [
                    '--format-options',
                    'headers.sort:false,json.sort_keys:false',
                    '--format-options=json.indent:10'
                ],
                {
                    'headers': {
                        'sort': False
                    },
                    'json': {
                        'sort_keys': False,
                        'indent': 10,
                        'format': True
                    },
                }
            ),
            (
                [
                    '--unsorted'
                ],
                {
                    'headers': {
                        'sort': False
                    },
                    'json': {
                        'sort_keys': False,
                        'indent': 4,
                        'format': True
                    },
                }
            ),
            (
                [
                    '--format-options=headers.sort:true',
                    '--unsorted',
                    '--format-options=headers.sort:true',
                ],
                {
                    'headers': {
                        'sort': True
                    },
                    'json': {
                        'sort_keys': False,
                        'indent': 4,
                        'format': True
                    },
                }
            ),
            (
                [
                    '--no-format-options',  # --no-<option> anywhere resets
                    '--format-options=headers.sort:true',
                    '--unsorted',
                    '--format-options=headers.sort:true',
                ],
                PARSED_DEFAULT_FORMAT_OPTIONS,
            ),
            (
                [
                    '--format-options=json.indent:2',
                    '--unsorted',
                    '--no-unsorted',
                ],
                {
                    'headers': {
                        'sort': True
                    },
                    'json': {
                        'sort_keys': True,
                        'indent': 2,
                        'format': True
                    },
                }
            ),
            (
                [
                    '--format-options=json.indent:2',
                    '--unsorted',
                    '--sorted',
                ],
                {
                    'headers': {
                        'sort': True
                    },
                    'json': {
                        'sort_keys': True,
                        'indent': 2,
                        'format': True
                    },
                }
            ),
            (
                [
                    '--format-options=json.indent:2',
                    '--sorted',
                    '--no-sorted',
                    '--no-unsorted',
                ],
                {
                    'headers': {
                        'sort': True
                    },
                    'json': {
                        'sort_keys': True,
                        'indent': 2,
                        'format': True
                    },
                }
            ),
        ],
    )
    def test_format_options_accumulation(self, args, expected_format_options):
        parsed_args = parser.parse_args(
            args=[*args, 'example.org'],
            env=MockEnvironment(),
        )
        assert parsed_args.format_options == expected_format_options
