from unittest.mock import patch

from django.test import Client, TestCase

from . import engine


class ParseDshortTests(TestCase):
    def test_single_group(self):
        self.assertEqual(engine.parse_dshort('2d6pips'), [
                         ('2', 'd6', 'pips', '', '', '')])

    def test_concatenated_groups(self):
        result = engine.parse_dshort('2d6pips3d4numr')
        self.assertEqual(result, [
            ('2', 'd6', 'pips', '', '', ''),
            ('3', 'd4', 'numr', '', '', ''),
        ])

    def test_custom_colors(self):
        result = engine.parse_dshort('5d10numr#FFA500#000000#808080')
        self.assertEqual(
            result, [('5', 'd10', 'numr', 'FFA500', '000000', '808080')])

    def test_empty_falls_back_to_0d6numr(self):
        self.assertEqual(engine.parse_dshort(
            ''), [('0', 'd6', 'numr', '', '', '')])

    def test_garbage_falls_back_to_0d6numr(self):
        self.assertEqual(engine.parse_dshort('not a dice string'), [
                         ('0', 'd6', 'numr', '', '', '')])


class ResolveColorTests(TestCase):
    def test_black_keyword(self):
        self.assertEqual(engine.resolve_color('black', '#FFFFFF'), '#000000')

    def test_white_keyword(self):
        self.assertEqual(engine.resolve_color('white', '#000000'), '#FFFFFF')

    def test_valid_hex(self):
        self.assertEqual(engine.resolve_color('228B22', '#000000'), '#228B22')

    def test_invalid_hex_uses_fallback(self):
        self.assertEqual(engine.resolve_color('ZZZZZZ', '#123456'), '#123456')

    def test_empty_uses_fallback(self):
        self.assertEqual(engine.resolve_color('', '#123456'), '#123456')


class RollDshortTests(TestCase):
    def test_dnum_matches_requested_count(self):
        result = engine.roll_dshort('4d6pips')
        self.assertEqual(result['dnum'], 4)
        self.assertEqual(len(result['increments']), 4)
        self.assertTrue(all(dicetype == 'd6' for dicetype,
                        _face in result['increments']))

    def test_repeated_faces_are_added_to_existing_stats_as_a_batch(self):
        engine.increment_ever([('d6', 3), ('d6', 3), ('d6', 4)])
        engine.increment_ever([('d6', 3), ('d6', 3)])

        stats = engine.ever_stats()['d6']
        self.assertEqual(stats['3'], 4)
        self.assertEqual(stats['4'], 1)

    def test_many_dice_use_a_fixed_number_of_stat_queries(self):
        increments = [('d6', 3)] * 20

        with self.assertNumQueries(5):
            engine.increment_ever(increments)

    def test_out_of_range_count_is_zeroed_not_clamped(self):
        result = engine.roll_dshort('25d6pips')
        self.assertEqual(result['dnum'], 0)
        self.assertEqual(result['roll_html'], '')

    def test_zero_count_produces_nothing(self):
        result = engine.roll_dshort('0d6pips')
        self.assertEqual(result['dnum'], 0)

    def test_d10_rolls_zero_instead_of_ten(self):
        with patch.object(engine, 'roll_die', return_value=10):
            result = engine.roll_dshort('1d10numr')
        self.assertEqual(result['increments'], [('d10', 10)])
        self.assertIn('>0</tspan>', result['roll_html'])
        self.assertNotIn('>10</tspan>', result['roll_html'])

    def test_pdrt_always_rolls_five_dice_with_off_def(self):
        result = engine.roll_dshort('1d6pdrt')
        self.assertEqual(result['dnum'], 5)
        self.assertIsNotNone(result['paydirt_off'])
        self.assertIsNotNone(result['paydirt_def'])
        self.assertGreaterEqual(result['paydirt_off'], 0)
        self.assertGreaterEqual(result['paydirt_def'], 0)

    def test_pdrt_normalized_dshort_forces_five(self):
        result = engine.roll_dshort('1d6pdrt')
        self.assertEqual(result['dshort'], '5d6pdrt')

    def test_dlds_always_rolls_three_dice(self):
        result = engine.roll_dshort('1d6dlds')
        self.assertEqual(result['dnum'], 3)

    def test_dlds_normalized_dshort_forces_three(self):
        result = engine.roll_dshort('7d6dlds')
        self.assertEqual(result['dshort'], '3d6dlds')

    def test_normalized_dshort_matches_input_for_non_fixed_groups(self):
        result = engine.roll_dshort('4d6pips')
        self.assertEqual(result['dshort'], '4d6pips')

    def test_normalized_dshort_preserves_custom_colors(self):
        result = engine.roll_dshort('1d6pips#111111#222222#333333')
        self.assertEqual(result['dshort'], '1d6pips#111111#222222#333333')

    def test_non_pdrt_leaves_paydirt_none(self):
        result = engine.roll_dshort('3d6pips')
        self.assertIsNone(result['paydirt_off'])
        self.assertIsNone(result['paydirt_def'])

    def test_data_die_attribute_matches_die_regex(self):
        result = engine.roll_dshort('1d6pips#111111#222222#333333')
        self.assertIn("data-die='1d6pips#111111#222222#333333'",
                      result['roll_html'])


class DicefaceShapeTests(TestCase):
    EXPECTED_SHAPES = {
        'pips': 'SS', 'cpip': 'SS', 'quid': 'SS',
        'rfdw': 'SS', 'rfdg': 'SS', 'rfdb': 'SS', 'rfdp': 'SS',
        'memr': 'SR', 'pdrt': 'SR', 'dlds': 'SR',
        'dddr': 'SR', 'dddb': 'SR', 'dddw': 'SR',
    }

    def test_each_diceface_produces_expected_shape_and_svg(self):
        for diceface, shape in self.EXPECTED_SHAPES.items():
            with self.subTest(diceface=diceface):
                result = engine.roll_dshort(f'1d6{diceface}')
                self.assertIn(f"class='{shape} F die'", result['roll_html'])
                self.assertIn('<svg', result['roll_html'])

    def test_numr_shape_per_dicetype(self):
        expectations = {'d4': 'TS', 'd6': 'SS', 'd8': 'TS',
                        'd10': 'KS', 'd12': 'PS', 'd20': 'TS'}
        for dicetype, shape in expectations.items():
            with self.subTest(dicetype=dicetype):
                result = engine.roll_dshort(f'1{dicetype}numr')
                self.assertIn(f"class='{shape} F die'", result['roll_html'])


class ViewTests(TestCase):
    def setUp(self):
        self.client = Client()

    def test_index_renders(self):
        response = self.client.get('/dice/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response, '<wa-callout id="roll_info" class="warning" variant="warning">')
        self.assertContains(response, '<wa-tab-group id="form_tabs" active="tabs-1">')
        self.assertContains(response, 'id="help_tabs"')
        self.assertContains(
            response, 'Help is available for all the dice types available.')
        self.assertContains(response, 'd20numr - Number')
        self.assertContains(response, 'd6cpip - Colored Pips')
        self.assertContains(response, 'd6memr - Memoir')
        self.assertNotContains(response, 'id="help_dialog"')
        content = response.content.decode()
        self.assertLess(content.index('id="form_tabs"'),
                        content.index('id="debug"'))
        self.assertLess(content.index('id="debug"'),
                        content.index('id="help_tabs"'))
        self.assertNotContains(response, 'fancyHR')

    def test_help_dice_are_grouped_by_number_and_d6_variants(self):
        response = self.client.get('/dice/')
        number_dice = [label.split(
            ' - ', 1)[0] for label, _div_id in response.context['help_number_dice']]
        d6_dice = [label.split(' - ', 1)[0]
                   for label, _div_id in response.context['help_d6_dice']]

        self.assertEqual(
            number_dice, ['d4numr', 'd6numr', 'd8numr', 'd10numr', 'd12numr', 'd20numr'])
        self.assertEqual(d6_dice, [
            'd6pips', 'd6cpip', 'd6memr', 'd6quid', 'd6pdrt',
            'd6dlds', 'd6rfdw', 'd6rfdg', 'd6rfdb', 'd6rfdp',
            'd6dddr', 'd6dddb', 'd6dddw',
        ])

    def test_help_examples_are_generated_only_for_requested_group(self):
        before = sum(
            row.ever_count for row in engine.DieFaceStat.objects.all())
        page = self.client.get('/dice/')
        after_page = sum(
            row.ever_count for row in engine.DieFaceStat.objects.all())
        self.assertEqual(after_page, before)
        self.assertContains(page, '<div id="d4_numr"></div>', html=True)

        response = self.client.get('/dice/help-examples/number')
        self.assertEqual(response.status_code, 200)
        examples = response.json()['examples']
        self.assertEqual(len(examples), 6)
        self.assertTrue(
            all('<svg' in example['roll_html'] for example in examples))
        after_examples = sum(
            row.ever_count for row in engine.DieFaceStat.objects.all())
        self.assertEqual(after_examples - before, 30)
        self.assertEqual(sum(response.json()['ever_stats']['d4'].values()), 5)
        self.assertEqual(sum(response.json()['ever_stats']['d6'].values()), 5)
        self.assertEqual(
            sum(response.json()['session_stats']['d4'].values()), 5)
        self.assertEqual(
            sum(response.json()['session_stats']['d6'].values()), 5)

    def test_custom_color_help_examples_include_svg_and_update_session_stats(self):
        response = self.client.get('/dice/help-examples/colors')

        self.assertEqual(response.status_code, 200)
        examples = response.json()['examples']
        self.assertEqual([example['div_id']
                         for example in examples], ['d10_color', 'd6_color'])
        self.assertTrue(
            all('<svg' in example['roll_html'] for example in examples))
        self.assertEqual(
            sum(response.json()['session_stats']['d10'].values()), 5)
        self.assertEqual(
            sum(response.json()['session_stats']['d6'].values()), 5)

    def test_help_examples_reject_unknown_group(self):
        response = self.client.get('/dice/help-examples/unknown')
        self.assertEqual(response.status_code, 404)

    def test_roll_tracks_history_and_stats(self):
        response = self.client.get('/dice/roll', {'dshort': '3d6pips'})
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data['numberrolls'], 1)
        self.assertEqual(sum(data['session_stats']['d6'].values()), 3)
        self.assertEqual(len(data['history']), 1)

    def test_roll_nohist_does_not_track(self):
        response = self.client.get(
            '/dice/roll', {'dshort': '3d6pips', 'nohist': 'true'})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(sorted(response.json().keys()),
                         ['dshort', 'roll_html'])

    def test_history_capped_at_eleven(self):
        for _ in range(15):
            self.client.get('/dice/roll', {'dshort': '1d6pips'})
        response = self.client.get('/dice/roll', {'dshort': '1d6pips'})
        self.assertLessEqual(response.json()['numberrolls'], 11)
        self.assertLessEqual(len(response.json()['history']), 11)
