import os
import sys
import json
import locale
import platform

import pytest

import radon.cli.tools as tools
from radon.visitors import Function, Class
from radon.raw import Module


def fake_isfile(filename):
    if filename == 'file.py':
        return True
    return False


def fake_walk(start):
    dirs = ['tests', 'sub', '.hid']
    contents = {'tests': ['test_amod.py', 'run.py', '.hid.py'],
                'sub': ['amod.py', 'bmod.py']}
    yield '.', dirs, ['tox.ini', 'amod.py', 'test_all.py', 'fake.yp', 'noext']
    for d in dirs:
        yield './{0}'.format(d), [], contents[d]


def fake_is_python_file(filename):
    return filename.endswith('.py')


def assert_pequal(a, b):
    a, b = [list(map(os.path.normpath, p)) for p in (a, b)]
    assert a == b


def test_open(mocker):
    with tools._open('-') as fobj:
        assert fobj is sys.stdin

    try:
        with tools._open(__file__) as fobj:
            assert True
    except TypeError:  # issue 101
        assert False, 'tools._open raised TypeError'

    m = mocker.mock_open()

    if platform.python_implementation() == 'PyPy':
        mocker.patch('radon.cli.tools.open', m, create=True)
        tools._open('randomfile.py').__enter__()
        m.assert_called_with('randomfile.py')
    else:
        mocker.patch('radon.cli.tools._open_function', m, create=True)
        tools._open('randomfile.py').__enter__()
        except_encoding = os.getenv('RADONFILESENCODING',
                                    locale.getpreferredencoding(False))
        m.assert_called_with('randomfile.py', encoding=except_encoding)


@pytest.fixture
def iter_files():
    return lambda *a, **kw: list(tools.iter_filenames(*a, **kw))


def test_iter_files_stdin(iter_files):
    assert iter_files(['-']) == ['-']


def test_iter_files(mocker, iter_files):
    os_mod = mocker.patch('radon.cli.tools.os')
    os_path_mod = mocker.patch('radon.cli.tools.os.path')
    os_path_mod.normpath = os.path.normpath
    os_path_mod.basename = os.path.basename
    os_path_mod.join = os.path.join
    os_path_mod.isfile.side_effect = fake_isfile
    os_mod.walk = fake_walk
    tools._is_python_file = fake_is_python_file

    assert_pequal(iter_files(['file.py', 'random/path']),
                  ['file.py', 'amod.py', 'test_all.py',
                   'tests/test_amod.py', 'tests/run.py', 'sub/amod.py',
                   'sub/bmod.py'])

    assert_pequal(iter_files(['file.py', 'random/path'], 'test_*'),
                  ['file.py', 'amod.py', 'tests/test_amod.py',
                   'tests/run.py', 'sub/amod.py', 'sub/bmod.py'])

    assert_pequal(iter_files(['file.py', 'random/path'], '*test_*'),
                  ['file.py', 'amod.py', 'tests/run.py', 'sub/amod.py',
                   'sub/bmod.py'])

    assert_pequal(iter_files(['file.py', 'random/path'], '*/test_*,amod*'),
                  ['file.py', 'test_all.py', 'tests/run.py',
                   'sub/amod.py', 'sub/bmod.py'])

    assert_pequal(iter_files(['file.py', 'random/path'], None, 'tests'),
                  ['file.py', 'amod.py', 'test_all.py', 'sub/amod.py',
                   'sub/bmod.py'])

    assert_pequal(iter_files(['file.py', 'random/path'], None, 'tests,sub'),
                  ['file.py', 'amod.py', 'test_all.py'])


CC_RESULTS_CASES = [
    (
        Function('name', 12, 0, 16, False, None, [], 6),
    {
        'type': 'function', 'name': 'name', 'lineno': 12, 'col_offset': 0,
        'endline': 16, 'closures': [], 'complexity': 6, 'rank': 'B',
    }),
    (
        Class('Classname', 17, 0, 29, [
            Function('name', 19, 4, 26, True, 'Classname', [], 7),
        ], [], 7),
    {
        'type': 'class', 'name': 'Classname', 'lineno': 17, 'col_offset': 0,
        'endline': 29, 'complexity': 7, 'rank': 'B', 'methods': [
            {
                'type': 'method', 'lineno': 19, 'col_offset': 4, 'endline': 26,
                'closures': [], 'complexity': 7, 'rank': 'B', 'classname':
                'Classname', 'name': 'name',
            }
        ],
    }),
    (
        Function('name', 12, 0, 16, False, None, [
            Function('aux', 13, 4, 17, False, None, [], 4),
        ], 10),
    {
        'type': 'function', 'name': 'name', 'lineno': 12, 'col_offset': 0,
        'endline': 16, 'complexity': 10, 'rank': 'B', 'closures': [
            {
                'name': 'aux', 'lineno': 13, 'col_offset': 4, 'endline': 17,
                'closures': [], 'complexity': 4, 'rank': 'A', 'type':
                'function',
            }
        ]
    }),
]


@pytest.mark.parametrize('blocks,dict_result', CC_RESULTS_CASES)
def test_cc_to_dict(blocks, dict_result):
    assert tools.cc_to_dict(blocks) == dict_result


CC_TO_XML_CASE = [
    {'closures': [], 'endline': 16, 'complexity': 6, 'lineno': 12, 'is_method':
     False, 'name': 'name', 'col_offset': 0, 'rank': 'B'},

    {'complexity': 8, 'endline': 29, 'rank': 'B', 'lineno': 17, 'name':
     'Classname', 'col_offset': 0},

    {'classname': 'Classname', 'closures': [], 'endline': 26, 'complexity': 7,
     'lineno': 19, 'is_method': True, 'name': 'name', 'col_offset': 4,
     'rank': 'B'},

    {'closures': [], 'endline': 17, 'complexity': 4, 'lineno': 13, 'is_method':
     False, 'name': 'aux', 'col_offset': 4, 'rank': 'A'},

    {'endline': 16, 'complexity': 10, 'lineno': 12, 'is_method': False, 'name':
     'name', 'col_offset': 0, 'rank': 'B'},
]


CC_TO_CODECLIMATE_CASE = [
    {'closures': [], 'endline': 16, 'complexity': 6, 'lineno': 12, 'type':
     'function', 'name': 'foo', 'col_offset': 0, 'rank': 'B'},

    {'complexity': 8, 'endline': 29, 'rank': 'B', 'lineno': 17, 'type': 'class',
    'name': 'Classname', 'col_offset': 0},

    {'closures': [], 'endline': 17, 'complexity': 4, 'lineno': 13, 'type':
     'method', 'name': 'bar', 'col_offset': 4, 'rank': 'A'},
]


def test_raw_to_dict():
    assert tools.raw_to_dict(Module(103, 123, 98, 8, 19, 5, 3)) == \
         {'loc': 103, 'lloc': 123, 'sloc': 98, 'comments': 8,
             'multi': 19, 'blank': 5, 'single_comments': 3}


def test_cc_to_xml():
    assert tools.dict_to_xml({'filename': CC_TO_XML_CASE}) == \
         '''<ccm>
              <metric>
                <complexity>6</complexity>
                <unit>name</unit>
                <classification>B</classification>
                <file>filename</file>
                <startLineNumber>12</startLineNumber>
                <endLineNumber>16</endLineNumber>
              </metric>
              <metric>
                <complexity>8</complexity>
                <unit>Classname</unit>
                <classification>B</classification>
                <file>filename</file>
                <startLineNumber>17</startLineNumber>
                <endLineNumber>29</endLineNumber>
              </metric>
              <metric>
                <complexity>7</complexity>
                <unit>Classname.name</unit>
                <classification>B</classification>
                <file>filename</file>
                <startLineNumber>19</startLineNumber>
                <endLineNumber>26</endLineNumber>
              </metric>
              <metric>
                <complexity>4</complexity>
                <unit>aux</unit>
                <classification>A</classification>
                <file>filename</file>
                <startLineNumber>13</startLineNumber>
                <endLineNumber>17</endLineNumber>
              </metric>
              <metric>
                <complexity>10</complexity>
                <unit>name</unit>
                <classification>B</classification>
                <file>filename</file>
                <startLineNumber>12</startLineNumber>
                <endLineNumber>16</endLineNumber>
              </metric>
            </ccm>'''.replace('\n', '').replace(' ', '')


def test_cc_error_to_codeclimate():
    error_result = {
        'error': 'Error: invalid syntax (<unknown>, line 100)'
    }

    expected_results = \
        [
            json.dumps({
                'description':'Error: Error: invalid syntax (<unknown>, line 100)',
                'check_name':'Complexity',
                'content': { 'body': 'We encountered an error attempting to analyze this line.' },
                'location': { 'path': 'filename', 'lines': {'begin': 100, 'end': 100}},
                'type':'issue',
                'categories': ['Bug Risk'],
                'remediation_points': 1000000,
                'fingerprint': '10ac332cd7f638664e8865b098a1707c'
                }),
        ]

    actual_results = tools.dict_to_codeclimate_issues({'filename': error_result})

    actual_sorted = []
    for i in actual_results:
         actual_sorted.append(json.loads(i))

    expected_sorted = []
    for i in expected_results:
         expected_sorted.append(json.loads(i))

    assert actual_sorted == expected_sorted


def test_cc_to_codeclimate():
    actual_results = tools.dict_to_codeclimate_issues({'filename': CC_TO_CODECLIMATE_CASE})
    expected_results = \
        [
            json.dumps({
                'description':'Cyclomatic complexity is too high in function foo. (6)',
                'check_name':'Complexity',
                'content': { 'body': tools.get_content()},
                'location': { 'path': 'filename', 'lines': {'begin': 12, 'end': 16}},
                'type':'issue',
                'categories': ['Complexity'],
                'remediation_points': 1100000,
                'fingerprint': 'afbe2b8d9a57fde5f3235ec97e7a22e1'
                }),
            json.dumps({
                'description':'Cyclomatic complexity is too high in class Classname. (8)',
                'check_name':'Complexity',
                'content': {'body': tools.get_content()},
                'location': {'path': 'filename', 'lines': {'begin': 17, 'end': 29}},
                'type':'issue',
                'categories': ['Complexity'],
                'remediation_points': 1300000,
                'fingerprint': '8caecbb525375d825b95c23bc8f881d7'
                }),
        ]

    actual_sorted = []
    for i in actual_results:
         actual_sorted.append(json.loads(i))

    expected_sorted = []
    for i in expected_results:
         expected_sorted.append(json.loads(i))

    assert actual_sorted == expected_sorted


CC_TO_TERMINAL_CASES = [
    Class(name='Classname', lineno=17, col_offset=0, endline=29,
          methods=[Function(name='meth', lineno=19, col_offset=4, endline=26,
                            is_method=True, classname='Classname', closures=[],
                            complexity=4)],
          inner_classes=[], real_complexity=4),
    Function(name='meth', lineno=19, col_offset=4, endline=26, is_method=True,
             classname='Classname', closures=[], complexity=7),
    Function(name='f1', lineno=12, col_offset=0, endline=16, is_method=False,
             classname=None, closures=[], complexity=14),
    Function(name='f2', lineno=12, col_offset=0, endline=16, is_method=False,
             classname=None, closures=[], complexity=22),
    Function(name='f3', lineno=12, col_offset=0, endline=16, is_method=False,
             classname=None, closures=[], complexity=32),
    Function(name='f4', lineno=12, col_offset=0, endline=16, is_method=False,
             classname=None, closures=[], complexity=41),
]


def test_cc_to_terminal():
    # do the patching
    tools.LETTERS_COLORS = dict((l, '<!{0}!>'.format(l)) for l in 'FMC')
    tools.RANKS_COLORS = dict((r, '<|{0}|>'.format(r)) for r in 'ABCDEF')
    tools.BRIGHT = '@'
    tools.RESET = '__R__'

    results = CC_TO_TERMINAL_CASES
    res = [
        '@<!C!>C __R__17:0 Classname - <|A|>A (4)__R__',
        '@<!M!>M __R__19:4 Classname.meth - <|B|>B (7)__R__',
        '@<!F!>F __R__12:0 f1 - <|C|>C (14)__R__',
        '@<!F!>F __R__12:0 f2 - <|D|>D (22)__R__',
        '@<!F!>F __R__12:0 f3 - <|E|>E (32)__R__',
        '@<!F!>F __R__12:0 f4 - <|F|>F (41)__R__',
    ]
    res_noshow = ['{0}__R__'.format(r[:r.index('(') - 1]) for r in res]

    assert tools.cc_to_terminal(results, False, 'A', 'F', False) == \
             (res_noshow, 120, 6)
    assert tools.cc_to_terminal(results, True, 'A', 'F', False) == \
             (res, 120, 6)
    assert tools.cc_to_terminal(results, True, 'A', 'D', False) == \
             (res[:-2], 47, 4)
    assert tools.cc_to_terminal(results, False, 'A', 'D', False) == \
             (res_noshow[:-2], 47, 4)
    assert tools.cc_to_terminal(results, True, 'C', 'F', False) == \
             (res[2:], 109, 4)
    assert tools.cc_to_terminal(results, True, 'B', 'E', False) == \
             (res[1:-1], 75, 4)
    assert tools.cc_to_terminal(results, True, 'B', 'F', True) == \
             (res[1:], 120, 6)
