# -*- coding: utf-8 -*-
from functools import partial
from itertools import cycle
import random
import unittest
import warnings

import six
from six import u

import coolname
from coolname import RandomGenerator, InitializationError
from coolname.exceptions import ConfigurationError
from coolname.loader import load_config

from .common import patch, TestCase, FakeRandom


class TestCoolname(TestCase):

    def test_slug(self):
        # Basic test, to check that it doesn't crash.
        # Output of default generator is always unicode.
        items = coolname.generate()
        self.assertIsInstance(items[0], six.text_type)
        name = coolname.generate_slug()
        self.assertIsInstance(name, six.text_type)
        self.assertGreater(len(name), 10)
        self.assertIn('-', name)

    def test_combinations(self):
        combinations_2 = 10**5
        combinations_3 = 10**8
        combinations_4 = 10**10
        self.assertGreater(coolname.get_combinations_count(), combinations_4)
        self.assertGreater(coolname.get_combinations_count(2), combinations_2)
        self.assertGreater(coolname.get_combinations_count(3), combinations_3)
        self.assertGreater(coolname.get_combinations_count(4), combinations_4)
        self.assertLess(coolname.get_combinations_count(3),
                        coolname.get_combinations_count())
        self.assertLess(coolname.get_combinations_count(4),
                        coolname.get_combinations_count())
        self.assertEqual(coolname.get_combinations_count(2) +
                         coolname.get_combinations_count(3) +
                         coolname.get_combinations_count(4),
                         coolname.get_combinations_count())

    @patch('os.path.isdir', return_value=False)
    @patch('os.path.isfile', return_value=False)
    def test_create_from_file_not_found(self, *args):
        with self.assertRaisesRegex(InitializationError,
                                    r'File or directory not found: .*dummy'):
            RandomGenerator(load_config('dummy'))

    @patch('os.path.isdir', return_value=False)
    @patch('os.path.isfile', return_value=True)
    @patch('coolname.loader._load_config')
    def test_create_from_file(self, load_config_mock, *args):
        load_config_mock.return_value = {
            'all': {
                'type': 'cartesian',
                'lists': ['number', 'number']
            },
            'number': {
                'type': 'words',
                'words': [str(x) for x in range(0, 10)]
            }
        }
        generator = RandomGenerator(load_config('dummy'))
        with patch.object(generator, '_randrange', return_value=35):
            self.assertEqual(generator.generate_slug(), '3-5')

    @patch('os.path.isdir', return_value=True)
    @patch('os.path.isfile', return_value=False)
    @patch('coolname.loader._load_data')
    def test_create_from_directory_conflict(self, load_data_mock, *args):
        load_data_mock.return_value = (
            {
                'all': {
                    'type': 'cartesian',
                    'lists': ['mywords']
                },
                'mywords': {
                    'type': 'words',
                    'words': ['this', 'is', 'a', 'conflict']
                }
            },
            {'mywords': ['a', 'b']})
        with self.assertRaisesRegex(InitializationError,
                                    r"^Conflict: list 'mywords' is defined both in config "
                                    "and in \*\.txt file. If it's a 'words' list, "
                                    "you should remove it from config\.$"):
            RandomGenerator(load_config('dummy'))

    def test_generate_by_pattern(self):
        generator = RandomGenerator({
            'all': {
                'type': 'cartesian',
                'lists': ['size', 'color', 'fruit'],
            },
            'justcolor': {
                'generator': True,
                'type': 'cartesian',
                'lists': ['color', 'fruit'],
            },
            'size': {
                'type': 'words',
                'words': ['small', 'large']
            },
            'color': {
                'type': 'words',
                'words': ['green', 'yellow']
            },
            'fruit': {
                'type': 'words',
                'words': ['apple', 'banana']
            },
        })
        with patch.object(generator, '_randrange', return_value=0):
            self.assertEqual(generator.generate_slug(), 'small-green-apple')
            self.assertEqual(generator.generate_slug('justcolor'), 'green-apple')

    def test_unicode_config(self):
        generator = RandomGenerator({
            u('all'): {
                u('type'): u('cartesian'),
                u('lists'): [u('прилагательное'), u('существительное')]
            },
            u('прилагательное'): {
                u('type'): u('words'),
                u('words'): [u('белый'), u('черный')]
            },
            u('существительное'): {
                u('type'): u('words'),
                u('words'): [u('круг'), u('квадрат')]
            }
        })
        with patch.object(generator, '_randrange',
                   side_effect=partial(next, cycle(iter(range(4))))):
            self.assertEqual(generator.generate_slug(), u('белый-круг'))
            self.assertEqual(generator.generate_slug(), u('белый-квадрат'))
            self.assertEqual(generator.generate_slug(), u('черный-круг'))
            self.assertEqual(generator.generate(), [u('черный'), u('квадрат')])

    def test_ensure_unique(self):
        # Test without ensure_unique - should yield repeats
        config = {
            'all': {
                'type': 'cartesian',
                'lists': ['adjective', 'of', 'noun'],
            },
            'adjective': {
                'type': 'words',
                'words': ['one', 'two']
            },
            'of': {
                'type': 'const',
                'value': 'of'
            },
            'noun': {
                'type': 'words',
                'words': ['one', 'two']
            }
        }
        generator = RandomGenerator(config)
        with patch.object(generator, '_randrange',
                          side_effect=partial(next, cycle(iter([0, 1, 2, 3])))):
            self.assertEqual(generator.generate_slug(), 'one-of-one')
            self.assertEqual(generator.generate_slug(), 'one-of-two')
            self.assertEqual(generator.generate_slug(), 'two-of-one')
            self.assertEqual(generator.generate_slug(), 'two-of-two')
            self.assertEqual(generator.generate_slug(), 'one-of-one')
        # Invalid ensure_unique
        config['all']['ensure_unique'] = 'qwe'
        with self.assertRaisesRegex(ConfigurationError, "Invalid config: Invalid ensure_unique value: expected boolean, got 'qwe'"):
            RandomGenerator(config)
        # Test with ensure_unique
        config['all']['ensure_unique'] = True
        with warnings.catch_warnings(record=True) as w:
            generator = RandomGenerator(config)
            if len(w) > 0:
                assert len(w) == 1
                assert str(w[0].message) == 'coolname.generate() may be slow because a significant fraction of combinations contain repeating words and ensure_unique is set'
        with patch.object(generator, '_randrange',
                          side_effect=partial(next, cycle(iter([0, 1, 2, 3])))):
            self.assertEqual(generator.generate_slug(), 'one-of-two')
            self.assertEqual(generator.generate_slug(), 'two-of-one')
            self.assertEqual(generator.generate_slug(), 'one-of-two')
            self.assertEqual(generator.generate_slug(), 'two-of-one')

    def test_ensure_unique_error(self):
        config = {
            'all': {'type': 'cartesian', 'lists': ['one', 'one']},
            'one': {'type': 'words', 'words': ['one', 'one']}
        }
        RandomGenerator(config)  # this is fine
        config['all']['ensure_unique'] = True
        with self.assertRaisesRegex(ConfigurationError, r'Invalid config: Impossible to generate with ensure_unique'):
            RandomGenerator(config)

    def test_ensure_unique_error_on_list(self):
        config = {
            'all': {'type': 'cartesian', 'lists': ['one', 'two']},
            'bad': {'type': 'cartesian', 'generator': True, 'lists': ['one', 'one']},
            'one': {'type': 'words', 'words': ['one', 'one']},
            'two': {'type': 'words', 'words': ['two', 'two']}
        }
        RandomGenerator(config)  # this is fine
        config['all']['ensure_unique'] = True
        with self.assertRaisesRegex(ConfigurationError, r'Invalid config: Impossible to generate with ensure_unique'):
            RandomGenerator(config)


    def test_ensure_unique_prefix(self):
        config = {
            'all': {
                'type': 'cartesian',
                'lists': ['w1', 'w2'],
            },
            'w1': {
                'type': 'words',
                'words': ['brave', 'agile']
            },
            'w2': {
                'type': 'words',
                'words': ['bravery',  'brass', 'agility', 'age']
            }
        }
        generator = RandomGenerator(config)
        with patch.object(generator, '_randrange',
                          side_effect=partial(next, cycle(iter(range(8))))):
            self.assertEqual(generator.generate_slug(), 'brave-bravery')  # This sucks

        # ensure_unique_prefix = 0 is not allowed
        config['all']['ensure_unique_prefix'] = 0
        with self.assertRaisesRegex(ConfigurationError, 'Invalid config: Invalid ensure_unique_prefix value: expected a positive integer, got 0'):
            RandomGenerator(config)

        # Now enable unique prefix
        config['all']['ensure_unique_prefix'] = 4
        generator = RandomGenerator(config)
        with patch.object(generator, '_randrange',
                          side_effect=partial(next, cycle(iter(range(8))))):
            self.assertEqual(generator.generate_slug(), 'brave-brass')
            self.assertEqual(generator.generate_slug(), 'brave-agility')
            self.assertEqual(generator.generate_slug(), 'brave-age')
            self.assertEqual(generator.generate_slug(), 'agile-bravery')
            self.assertEqual(generator.generate_slug(), 'agile-brass')
            self.assertEqual(generator.generate_slug(), 'agile-age')
            self.assertEqual(generator.generate_slug(), 'brave-brass')

    def test_configuration_error(self):
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Value at key 'all' is not a dict"):
            RandomGenerator({'all': ['wrong']})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has no 'type'"):
            RandomGenerator({'all': {'typ': 'wrong'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'type'"):
            RandomGenerator({'all': {'type': 'wrong'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has no 'lists'"):
            RandomGenerator({'all': {'type': 'nested'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'lists'"):
            RandomGenerator({'all': {'type': 'nested', 'lists': 'wrong'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has no 'value'"):
            RandomGenerator({'all': {'type': 'const'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'value'"):
            RandomGenerator({'all': {'type': 'const', 'value': 123}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has no 'words'"):
            RandomGenerator({'all': {'type': 'words'}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'words'"):
            RandomGenerator({'all': {'type': 'words', 'words': []}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Lists are referenced but not defined: one, two"):
            RandomGenerator({'all': {'type': 'nested', 'lists': ['one', 'two']}})
        with self.assertRaisesRegex(InitializationError,
                                   "Invalid config: Rule 'all' is recursive: \['all', 'one'\]"):
            RandomGenerator({
                'all': {'type': 'nested', 'lists': ['one']},
                'one': {'type': 'nested', 'lists': ['all']}
            })

    def test_configuration_error_phrases(self):
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has no 'phrases'"):
            RandomGenerator({'all': {'type': 'phrases', 'words': []}})
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'phrases'"):
            RandomGenerator({'all': {'type': 'phrases', 'phrases': []}})
        generator = RandomGenerator({'all': {'type': 'phrases', 'phrases': ['str is allowed']}})
        assert generator.generate_slug() == 'str-is-allowed'
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid 'phrases': must be all string/tuple/list"):
            RandomGenerator({'all': {'type': 'phrases', 'phrases': [[['too many square brackets']]]}})
        # Number of words
        RandomGenerator({
            'all': {
                'type': 'phrases',
                'number_of_words': 2,
                'phrases': [['one', 'two'], ['three', 'four']]}
        })
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid phrase 'five' \(1 word\(s\) but number_of_words=2\)"):
            RandomGenerator({
                'all': {
                    'type': 'phrases',
                    'number_of_words': 2,
                    'phrases': [['one', 'two'], ['three', 'four'], ['five']]}
            })
        # Max length
        RandomGenerator({
            'all': {
                'type': 'phrases',
                'max_length': 10,
                'phrases': [['black', 'goose'], ['white', 'hare']]}
        })
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Config at key 'all' has invalid phrase 'white rabbit' \(longer than 10 characters\)"):
            RandomGenerator({
                'all': {
                    'type': 'phrases',
                    'max_length': 10,
                    'phrases': [['black', 'goose'], ['white', 'rabbit']]}
            })

    def test_max_length(self):
        with self.assertRaisesRegex(InitializationError,
                                   "Config at key u?'one' has invalid word u?'tiger' "
                                   "\(longer than 4 characters\)"):
            RandomGenerator({
                'all': {'type': 'nested', 'lists': ['one']},
                'one': {'type': 'words', 'max_length': 4, 'words': ['cat', 'lion', 'tiger']}
            })

    def test_max_slug_length_invalid(self):
        with self.assertRaisesRegex(InitializationError,
                                    r'Invalid config: Invalid max_slug_length value'):
            RandomGenerator({
                'all': {'type': 'words', 'max_slug_length': 'invalid', 'words': ['one', 'two']},
            })

    def test_max_slug_length(self):
        with warnings.catch_warnings(record=True) as w:
            generator = RandomGenerator({
                'all': {'type': 'cartesian', 'max_slug_length': 9, 'lists': ['one', 'two']},
                'one': {'type': 'words', 'words': ['big',  'small']},
                'two': {'type': 'words', 'words': ['cat',  'tiger']},
            })
            if len(w) > 0:
                assert len(w) == 1
                assert str(w[0].message) == 'coolname.generate() may be slow because a significant fraction of combinations exceed max_slug_length=9'
        self.assertEqual(set(generator.generate_slug() for i in range(0, 100)),
                         set(['big-cat', 'big-tiger', 'small-cat']))

    def test_max_slug_length_too_small(self):
        badlist = [str(i) for i in range(10, 100)]
        with self.assertRaisesRegex(InitializationError,
                                    r'Invalid config: Impossible to generate '
                                    r'with max_slug_length=3'):
            RandomGenerator({
                'all': {'type': 'cartesian', 'max_slug_length': 3, 'lists': ['one', 'two']},
                'one': {'type': 'words', 'words': badlist},
                'two': {'type': 'words', 'words': badlist},
            })

    @patch('warnings.warn')
    def test_max_slug_length_warning(self, warn_mock):
        RandomGenerator({
            'all': {'type': 'cartesian', 'max_slug_length': 3, 'lists': ['one', 'two']},
            'one': {'type': 'words', 'words': ['a']*70 + ['bb']*30},
            'two': {'type': 'words', 'words': ['c']*70 + ['dd']*30},
        })
        warn_mock.assert_called_with('coolname.generate() may be slow because a significant '
                                     'fraction of combinations exceed max_slug_length=3')

    def test_configuration_error_too_deep(self):
        config = {
            'all': {
                'type': 'nested',
                'lists': ['list0']
            },
            'list100': {
                'type': 'words',
                'words': ['too', 'deep', 'for', 'you'],
            }
        }
        for i in range(100):
            config['list{}'.format(i)] = {'type': 'nested', 'lists': ['list{}'.format(i+1)]}
        with self.assertRaisesRegex(InitializationError,
                                    "Invalid config: Rule 'all' is too deep"):
            RandomGenerator(config)


    @patch('coolname.impl.randrange', side_effect=partial(next, cycle(iter(range(8)))))
    def test_configuration_error_cartesian_inside_cartesian(self, mock):
        config = {
            'all': {
                'type': 'cartesian',
                'lists': ['word_list', 'cart_list']
            },
            'word_list': {
                'type': 'words',
                'words': ['word1', 'word2'],
            },
            'cart_list': {
                'type': 'cartesian',
                'lists': ['word_list', 'word_list'],
            },
        }
        with self.assertRaisesRegex(InitializationError,
                                    r"Invalid config: Cartesian list 'all' contains "
                                    r"another Cartesian list 'cart_list'\. Nested Cartesian lists "
                                    r"are not allowed\."):
            RandomGenerator(config)

    def test_mix_phrases_and_words_in_nested_list(self):
        config = {
            'all': {
                'type': 'cartesian',
                'lists': ['a', 'nested']
            },
            'a': {
                'type': 'const',
                'value': 'a'
            },
            'nested': {
                'type': 'nested',
                'lists': ['words', 'phrases']
            },
            'words': {
                'type': 'words',
                'words': ['one', 'two']
            },
            'phrases': {
                'type': 'phrases',
                'phrases': [
                    'three four',    # Can be space-separated string
                    ['five', 'six']  # or a list/tuple
                ]
            }
        }
        generator = RandomGenerator(config)
        random.seed(0)
        values = set(generator.generate_slug() for i in range(28))
        self.assertEqual(values, set(['a-one', 'a-two', 'a-three-four', 'a-five-six']))

    def test_random_default(self):
        # NOTE: four slugs in this test must be updated every time you change word lists

        # 1. Re-seed default generator
        random.seed(123)
        assert random.random() == 0.052363598850944326
        if six.PY2:  # randrange returns different results in Python 2
            self.assertEqual(coolname.generate_slug(), six.u('smooth-tuscan-gecko-of-success'))
        else:
            self.assertEqual(coolname.generate_slug(), 'hypersonic-goat-of-sheer-downpour')

        # 2. Replace default generator
        rand = random.Random()
        rand.seed(456)
        assert rand.random() == 0.7482025358782363
        coolname.replace_random(rand)
        if six.PY2:
            self.assertEqual(coolname.generate_slug(), six.u('resilient-inventive-cricket-from-shambhala'))
        else:
            self.assertEqual(coolname.generate_slug(), 'groovy-cerise-turkey-of-opportunity')

        # 3. Custom generator with custom Random
        config = {
            'all': {
                'type': 'cartesian',
                'lists': ['digits', 'digits']
            },
            'digits': {
                'type': 'words',
                'words': list(str(x) for x in range(10))
            }
        }
        generator = RandomGenerator(config)
        generator.random.seed(12)
        if six.PY2:
            self.assertEqual(generator.generate_slug(), '4-7')
        else:
            self.assertEqual(generator.generate_slug(), '6-0')
        generator.random = FakeRandom(33)
        if six.PY2:
            generator.generate_slug() == '3-3'
        else:
            generator.generate_slug() == '3-3'


if __name__ == '__main__':
    import sys
    sys.exit(unittest.main())
