import sys
import threading
from pathlib import Path

import pytest

from thriftpy2.thrift import TType
from thriftpy2.parser import load, load_fp
from thriftpy2.parser.exc import ThriftParserError, ThriftGrammarError

TEST_DIR = Path(__file__).parent


def test_comments():
    load(TEST_DIR / 'parser-cases/comments.thrift')


def test_constants():
    thrift = load(TEST_DIR / 'parser-cases/constants.thrift')
    assert thrift.tbool is True
    assert thrift.tboolint is True
    assert thrift.tbyte == 3
    assert thrift.int8 == 3
    assert thrift.int16 == 3
    assert thrift.int32 == 800
    assert thrift.int64 == 123456789
    assert thrift.tstr == 'hello world'
    assert thrift.integer32 == 900
    assert thrift.tdouble == 1.3
    assert thrift.tlist == [1, 2, 3]
    assert thrift.tset == {1, 2, 3}
    assert thrift.tmap1 == {'key': 'val'}
    assert thrift.tmap2 == {'key': 32}
    assert thrift.my_country == 4
    assert thrift.tom == thrift.Person(name='tom')
    assert thrift.country_map == {1: 'US', 2: 'UK', 3: 'CA', 4: 'CN'}


def test_include():
    thrift = load(TEST_DIR / 'parser-cases/include.thrift', include_dirs=[
        TEST_DIR / 'parser-cases'], module_name='include_thrift')
    assert thrift.datetime == 1422009523
    assert sys.modules['include_thrift'] is not None
    assert sys.modules['included_thrift'] is not None
    assert sys.modules['include.included_1_thrift'] is not None
    assert sys.modules['include.included_2_thrift'] is not None


def test_include_with_module_name_prefix():
    load(TEST_DIR / 'parser-cases/include.thrift', module_name='parser_cases.include_thrift')
    assert sys.modules['parser_cases.include_thrift'] is not None
    assert sys.modules['parser_cases.included_thrift'] is not None
    assert sys.modules['parser_cases.include.included_1_thrift'] is not None
    assert sys.modules['parser_cases.include.included_2_thrift'] is not None


def test_include_conflict():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/foo.bar.thrift', module_name='foo.bar_thrift')
    assert 'Module name conflict between' in str(excinfo.value)


def test_cpp_include():
    load(TEST_DIR / 'parser-cases/cpp_include.thrift')


def test_load_in_sub_thread(reraise):
    @reraise.wrap
    def f():
        load(TEST_DIR / 'addressbook.thrift')

    t = threading.Thread(target=f)
    t.start()
    t.join()


def test_load_fp_in_sub_thread(reraise):
    @reraise.wrap
    def f():
        with open(TEST_DIR / 'container.thrift') as fp:
            load_fp(fp, 'container_thrift')

    t = threading.Thread(target=f)
    t.start()
    t.join()


def test_tutorial():
    thrift = load(TEST_DIR / 'parser-cases/tutorial.thrift', include_dirs=[
        str(TEST_DIR / 'parser-cases')])
    assert thrift.INT32CONSTANT == 9853
    assert thrift.MAPCONSTANT == {'hello': 'world', 'goodnight': 'moon'}
    assert thrift.Operation.ADD == 1 and thrift.Operation.SUBTRACT == 2 \
        and thrift.Operation.MULTIPLY == 3 and thrift.Operation.DIVIDE == 4
    work = thrift.Work()
    assert work.num1 == 0 and work.num2 is None and work.op is None \
        and work.comment is None
    assert set(thrift.Calculator.thrift_services) == set([
        'ping', 'add', 'calculate', 'zip', 'getStruct'])


def test_e_type_error():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_type_error_0.thrift')
    assert 'Type error' in str(excinfo.value)

    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_type_error_1.thrift')
    assert 'Type error' in str(excinfo.value)

    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_type_error_2.thrift')
    assert 'Type error' in str(excinfo.value)


def test_value_ref():
    thrift = load(TEST_DIR / 'parser-cases/value_ref.thrift')
    assert thrift.container == {'key': [1, 2, 3]}
    assert thrift.lst == [39, 899, 123]


def test_type_ref():
    thrift = load(TEST_DIR / 'parser-cases/type_ref.thrift')
    assert thrift.jerry == thrift.type_ref_shared.Writer(
        name='jerry', age=26, country=thrift.type_ref_shared.Country.US)
    assert thrift.book == thrift.type_ref_shared.Book(name='Hello World',
                                                      writer=thrift.jerry)


def test_e_value_ref():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_value_ref_0.thrift')
    assert excinfo.value

    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_value_ref_1.thrift')
    assert str(excinfo.value) == ('Couldn\'t find a named value in enum Lang '
                                  'for value 3')
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_value_ref_2.thrift')
    assert str(excinfo.value) == \
        'No enum value or constant found named \'Cookbook\''


def test_enums():
    thrift = load(TEST_DIR / 'parser-cases/enums.thrift')
    assert thrift.Lang.C == 0
    assert thrift.Lang.Go == 1
    assert thrift.Lang.Java == 2
    assert thrift.Lang.Javascript == 3
    assert thrift.Lang.PHP == 4
    assert thrift.Lang.Python == 5
    assert thrift.Lang.Ruby == 6
    assert thrift.Country.US == 1
    assert thrift.Country.UK == 2
    assert thrift.Country.CN == 3
    assert thrift.OS.OSX == 0
    assert thrift.OS.Win == 3
    assert thrift.OS.Linux == 4


def test_structs():
    thrift = load(TEST_DIR / 'parser-cases/structs.thrift')
    assert thrift.Person.thrift_spec == {
        1: (TType.STRING, 'name', False),
        2: (TType.STRING, 'address', False)
    }
    assert thrift.Person.default_spec == [
        ('name', None), ('address', None)
    ]
    assert thrift.MetaData.thrift_spec == {
        1: (TType.SET, 'tags', TType.STRING, False)
    }
    assert thrift.Email.thrift_spec == {
        1: (TType.STRING, 'subject', False),
        2: (TType.STRING, 'content', False),
        3: (TType.STRUCT, 'sender', thrift.Person, False),
        4: (TType.STRUCT, 'recver', thrift.Person, True),
        5: (TType.STRUCT, 'metadata', thrift.MetaData, False),
    }
    assert thrift.Email.default_spec == [
        ('subject', 'Subject'), ('content', None),
        ('sender', None), ('recver', None), ('metadata', None)
    ]
    assert thrift.email == thrift.Email(
        subject='Hello',
        content='Long time no see',
        sender=thrift.Person(name='jack', address='jack@gmail.com'),
        recver=thrift.Person(name='chao', address='chao@gmail.com'),
        metadata=thrift.MetaData(tags=set())
    )
    assert thrift.Dog.thrift_spec == {
        -1: (TType.STRING, 'name', False),
        1: (TType.I32, 'age', False),
        -2: (TType.STRING, 'nickname', False)
    }
    assert thrift.Cat.thrift_spec == {
        -1: (TType.STRING, 'name', False),
    }


def test_e_structs():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_structs_0.thrift')
    assert str(excinfo.value) == \
        'Field \'name\' was required to create constant for type \'User\''

    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_structs_1.thrift')
    assert str(excinfo.value) == \
        'No field named \'avatar\' was found in struct of type \'User\''


def test_service():
    thrift = load(TEST_DIR / 'parser-cases/service.thrift')
    assert thrift.EmailService.thrift_services == ['ping', 'send', 'receive', 'empty']
    assert thrift.EmailService.ping_args.thrift_spec == {}
    assert thrift.EmailService.ping_args.default_spec == []
    assert thrift.EmailService.ping_result.thrift_spec == {
        1: (TType.STRUCT, 'network_error', thrift.NetworkError, False)
    }
    assert thrift.EmailService.ping_result.default_spec == [
        ('network_error', None)
    ]
    assert thrift.EmailService.send_args.thrift_spec == {
        1: (TType.STRUCT, 'recver', thrift.User, False),
        2: (TType.STRUCT, 'sender', thrift.User, False),
        3: (TType.STRUCT, 'email', thrift.Email, False),
    }
    assert thrift.EmailService.send_args.default_spec == [
        ('recver', None), ('sender', None), ('email', None)
    ]
    assert thrift.EmailService.send_result.thrift_spec == {
        0: (TType.BOOL, 'success', False),
        1: (TType.STRUCT, 'network_error', thrift.NetworkError, False)
    }
    assert thrift.EmailService.send_result.default_spec == [
        ('success', None), ('network_error', None)
    ]
    assert thrift.EmailService.receive_args.thrift_spec == {
        -1: (TType.STRUCT, 'recver', thrift.Email, False),
        12: (TType.STRUCT, 'user', thrift.User, False),
    }
    assert thrift.EmailService.empty_args.thrift_spec == {
        -1: (TType.STRUCT, 'recver', thrift.Email, False),
    }


def test_service_extends():
    thrift = load(TEST_DIR / 'parser-cases/service_extends.thrift')
    assert thrift.PingService.thrift_services == ['ping', 'getStruct']


def test_e_service_extends():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_service_extends_0.thrift')
    assert 'Can\'t find service' in str(excinfo.value)


def test_e_dead_include():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_dead_include_0.thrift')
    assert 'Dead including' in str(excinfo.value)


def test_e_grammar_error_at_eof():
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_grammar_error_at_eof.thrift')
    thrift_file = TEST_DIR / 'parser-cases/e_grammar_error_at_eof.thrift'
    assert str(excinfo.value) == "Grammar error at EOF of the file '{}'".format(thrift_file)


def test_e_grammar_error_at_token():
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_grammar_error_token.thrift')
    thrift_file = TEST_DIR / 'parser-cases/e_grammar_error_token.thrift'
    assert str(excinfo.value) == \
        "Grammar error '}' at line 3 of the file '%s'" % thrift_file


def test_e_use_thrift_reserved_keywords():
    with pytest.raises(ThriftParserError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_use_thrift_reserved_keywords.thrift')
    assert 'Cannot use reserved language keyword' in str(excinfo.value)


def test_e_duplicate_field_id_or_name():
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_field_id.thrift')
    assert 'field identifier/name has already been used' in str(excinfo.value)
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_field_name.thrift')
    assert 'field identifier/name has already been used' in str(excinfo.value)


def test_e_duplicate_struct_exception_service():
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_struct.thrift')
    assert 'type is already defined in' in str(excinfo.value)
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_exception.thrift')
    assert 'type is already defined in' in str(excinfo.value)
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_service.thrift')
    assert 'type is already defined in' in str(excinfo.value)


def test_e_duplicate_function():
    with pytest.raises(ThriftGrammarError) as excinfo:
        load(TEST_DIR / 'parser-cases/e_duplicate_function.thrift')
    assert 'function is already defined in' in str(excinfo.value)


def test_thrift_meta():
    thrift = load(TEST_DIR / 'parser-cases/tutorial.thrift')
    meta = thrift.__thrift_meta__
    assert meta['consts'] == [thrift.INT32CONSTANT, thrift.MAPCONSTANT]
    assert meta['enums'] == [thrift.Operation]
    assert meta['structs'] == [thrift.Work]
    assert meta['exceptions'] == [thrift.InvalidOperation]
    assert meta['services'] == [thrift.Calculator]
    assert meta['includes'] == [thrift.shared]


def test_thrift_namespaces():
    thrift = load(TEST_DIR / 'parser-cases/namespaces.thrift')
    assert thrift.__thrift_namespaces__ == {
        '*': 'default_ns',
        'py': 'foo.baz',
        'py.twisted': 'foo.bar.twisted',
        'java': 'com.example.foo',
    }


def test_thrift_namespaces_empty():
    thrift = load(TEST_DIR / 'parser-cases/comments.thrift')
    assert thrift.__thrift_namespaces__ == {}


def test_thrift_namespaces_load_fp():
    with open(TEST_DIR / 'parser-cases/namespaces.thrift') as thrift_fp:
        thrift = load_fp(thrift_fp, 'namespaces_thrift')
    assert thrift.__thrift_namespaces__['py'] == 'foo.baz'


def test_load_fp():
    # `shared.thrift` may already be cached under 'shared_thrift' (with its
    # __name__ rewritten) from being included by another test; clear the
    # cache so it is parsed fresh as a top-level module here.
    from thriftpy2.parser import parser as _parser
    _parser._thrift_cache.clear()

    thrift = None
    with open(TEST_DIR / 'parser-cases/shared.thrift') as thrift_fp:
        thrift = load_fp(thrift_fp, 'shared_thrift')
    assert thrift.__name__ == 'shared_thrift'
    assert thrift.__thrift_file__ is None
    assert thrift.__thrift_meta__['structs'] == [thrift.SharedStruct]
    assert thrift.__thrift_meta__['services'] == [thrift.SharedService]


def test_e_load_fp():
    with pytest.raises(ThriftParserError) as excinfo:
        with open(TEST_DIR / 'parser-cases/tutorial.thrift') as thrift_fp:
            load_fp(thrift_fp, 'tutorial_thrift')
        assert ('Unexpected include statement while loading '
                'from file like object.') == str(excinfo.value)


def test_recursive_union():
    thrift = load(TEST_DIR / 'parser-cases/recursive_union.thrift')
    assert thrift.Dynamic.thrift_spec == {
        1: (TType.BOOL, 'boolean', False),
        2: (TType.I64, 'integer', False),
        3: (TType.DOUBLE, 'doubl', False),
        4: (TType.STRING, 'str', False),
        5: (TType.LIST, 'arr', (TType.STRUCT, thrift.Dynamic), False),
        6: (TType.MAP, 'object', (TType.STRING, (TType.STRUCT,
                                                 thrift.Dynamic)), False)
    }


def test_issue_215():
    thrift = load(TEST_DIR / 'parser-cases/issue_215.thrift')
    assert thrift.abool is True
    assert thrift.falseValue == 123


def test_doubles():
    thrift = load(TEST_DIR / 'parser-cases/doubles.thrift')
    book = thrift.Book()
    assert book.price == 1
    assert isinstance(book.price, float)
    assert isinstance(thrift.value1, float) and thrift.value1 == 3
    assert isinstance(thrift.value2, float) and thrift.value2 == 3.1
    assert isinstance(thrift.value3, float) and thrift.value3 == 100000.0
    assert isinstance(thrift.value4, float) and thrift.value4 == -1.5e-05
    assert isinstance(thrift.value5, float) and thrift.value5 == 150000.0
    assert isinstance(thrift.value6, float) and thrift.value6 == 0.13


def test_struct_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.foo.__thrift_annotations__ == {
        'cpp.type': 'DenseFoo',
        'python.type': 'DenseFoo',
        'java.final': '',
        'annotation.without.value': None,
    }
    assert thrift.foo.__thrift_field_annotations__ == {
        'bar': {'presence': 'required'},
        'baz': {'presence': 'manual', 'cpp.use_pointer': ''},
    }
    # Fields without annotations should not be in the dict
    assert 'qux' not in thrift.foo.__thrift_field_annotations__
    assert 'bop' not in thrift.foo.__thrift_field_annotations__


def test_exception_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.foo_error.__thrift_annotations__ == {'foo': 'bar'}
    assert thrift.foo_error.__thrift_field_annotations__ == {
        'error_code': {'foo': 'bar'},
    }


def test_enum_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.weekdays.__thrift_annotations__ == {'foo.bar': 'baz'}
    assert thrift.weekdays.__thrift_item_annotations__ == {
        'SUNDAY': {'weekend': 'yes'},
        'SATURDAY': {'weekend': 'yes'},
    }


def test_service_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.foo_service.__thrift_annotations__ == {'a.b': 'c'}
    assert thrift.foo_service.__thrift_function_annotations__ == {
        'foo': {'foo': 'bar'},
    }


def test_union_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.foo_union.__thrift_annotations__ == {'a.b': 'c'}


def test_typedef_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.__thrift_typedef_annotations__ == {
        'non_latin_string': {'foo': 'bar'},
    }


def test_const_annotations():
    thrift = load(TEST_DIR / 'parser-cases/annotations.thrift')
    assert thrift.__thrift_const_annotations__ == {
        'id': {'name': 'LANG_ID'},
    }


def test_annotations_issue_252():
    load(TEST_DIR / 'parser-cases/issue_252.thrift')


def test_nest_incomplete_type():
    thrift = load(TEST_DIR / 'parser-cases/nest_incomplete_type.thrift')
    assert thrift.Container.thrift_spec == {
        1: (15, 'field1', (13, (11, (12, thrift.A))), False),
        2: (15, 'field2', (15, (12, thrift.A)), False),
        3: (15, 'field3', (15, (15, (12, thrift.A))), False)
    }


def test_thrift_in_include_path():
    """Regression: directory named 'thrift' in include path should not be mangled.

    Previously, .replace(".thrift", "_thrift") was applied greedily to the
    entire dotted module path, so a directory literally named 'thrift/' would
    get its dot-separator replaced (e.g. 'main.thrift.sub' -> 'main_thrift.sub').
    The fix only replaces the final .thrift file extension suffix.
    """
    thrift = load(
        TEST_DIR / 'parser-cases/thrift_in_path/parent.thrift',
        module_name='thrift_in_path_parent_thrift',
    )
    # 'thrift' in the directory path must be preserved as '.thrift.' not '_thrift'
    assert sys.modules['thrift_in_path_main.thrift.sub.child_thrift'] is not None
    assert thrift.child.ChildStruct is not None
    # The old greedy replace would have produced this mangled key — verify it's absent
    assert 'thrift_in_path_main_thrift.sub.child_thrift' not in sys.modules


def test_issue_121():
    load(TEST_DIR / 'parser-cases/issue_121.thrift')
