from django import forms
from django.test import TestCase

from dcim.models import Site
from netbox.choices import ImportFormatChoices
from utilities.forms.bulk_import import BulkImportForm
from utilities.forms.fields.csv import CSVSelectWidget
from utilities.forms.fields.dynamic import DynamicChoiceField, DynamicMultipleChoiceField
from utilities.forms.forms import BulkRenameForm
from utilities.forms.utils import (
    expand_alphanumeric_pattern,
    expand_ipnetwork_pattern,
    get_capacity_unit_label,
    get_field_value,
)
from utilities.forms.widgets.select import AvailableOptions, SelectedOptions


class ExpandIPNetworkTestCase(TestCase):
    """
    Validate the operation of expand_ipnetwork_pattern().
    """
    def test_ipv4_range(self):
        input = '1.2.3.[9-10]/32'
        output = sorted([
            '1.2.3.9/32',
            '1.2.3.10/32',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 4)), output)

    def test_ipv4_set(self):
        input = '1.2.3.[4,44]/32'
        output = sorted([
            '1.2.3.4/32',
            '1.2.3.44/32',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 4)), output)

    def test_ipv4_multiple_ranges(self):
        input = '1.[9-10].3.[9-11]/32'
        output = sorted([
            '1.9.3.9/32',
            '1.9.3.10/32',
            '1.9.3.11/32',
            '1.10.3.9/32',
            '1.10.3.10/32',
            '1.10.3.11/32',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 4)), output)

    def test_ipv4_multiple_sets(self):
        input = '1.[2,22].3.[4,44]/32'
        output = sorted([
            '1.2.3.4/32',
            '1.2.3.44/32',
            '1.22.3.4/32',
            '1.22.3.44/32',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 4)), output)

    def test_ipv4_set_and_range(self):
        input = '1.[2,22].3.[9-11]/32'
        output = sorted([
            '1.2.3.9/32',
            '1.2.3.10/32',
            '1.2.3.11/32',
            '1.22.3.9/32',
            '1.22.3.10/32',
            '1.22.3.11/32',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 4)), output)

    def test_ipv6_range(self):
        input = 'fec::abcd:[9-b]/64'
        output = sorted([
            'fec::abcd:9/64',
            'fec::abcd:a/64',
            'fec::abcd:b/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_ipv6_range_multichar_field(self):
        input = 'fec::abcd:[f-11]/64'
        output = sorted([
            'fec::abcd:f/64',
            'fec::abcd:10/64',
            'fec::abcd:11/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_ipv6_set(self):
        input = 'fec::abcd:[9,ab]/64'
        output = sorted([
            'fec::abcd:9/64',
            'fec::abcd:ab/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_ipv6_multiple_ranges(self):
        input = 'fec::[1-2]bcd:[9-b]/64'
        output = sorted([
            'fec::1bcd:9/64',
            'fec::1bcd:a/64',
            'fec::1bcd:b/64',
            'fec::2bcd:9/64',
            'fec::2bcd:a/64',
            'fec::2bcd:b/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_ipv6_multiple_sets(self):
        input = 'fec::[a,f]bcd:[9,ab]/64'
        output = sorted([
            'fec::abcd:9/64',
            'fec::abcd:ab/64',
            'fec::fbcd:9/64',
            'fec::fbcd:ab/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_ipv6_set_and_range(self):
        input = 'fec::[dead,beaf]:[9-b]/64'
        output = sorted([
            'fec::dead:9/64',
            'fec::dead:a/64',
            'fec::dead:b/64',
            'fec::beaf:9/64',
            'fec::beaf:a/64',
            'fec::beaf:b/64',
        ])

        self.assertEqual(sorted(expand_ipnetwork_pattern(input, 6)), output)

    def test_invalid_address_family(self):
        with self.assertRaisesRegex(Exception, 'Invalid IP address family: 5'):
            sorted(expand_ipnetwork_pattern(None, 5))

    def test_invalid_non_pattern(self):
        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.4/32', 4))

    def test_invalid_range(self):
        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[4-]/32', 4))

        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[-4]/32', 4))

        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[4--5]/32', 4))

    def test_invalid_range_bounds(self):
        self.assertEqual(sorted(expand_ipnetwork_pattern('1.2.3.[4-3]/32', 6)), [])

    def test_invalid_set(self):
        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[4]/32', 4))

        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[4,]/32', 4))

        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[,4]/32', 4))

        with self.assertRaises(ValueError):
            sorted(expand_ipnetwork_pattern('1.2.3.[4,,5]/32', 4))


class ExpandAlphanumericTestCase(TestCase):
    """
    Validate the operation of expand_alphanumeric_pattern().
    """
    def test_range_numberic(self):
        input = 'r[9-11]a'
        output = sorted([
            'r9a',
            'r10a',
            'r11a',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_range_alpha(self):
        input = '[r-t]1a'
        output = sorted([
            'r1a',
            's1a',
            't1a',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_set_numeric(self):
        input = 'r[1,2]a'
        output = sorted([
            'r1a',
            'r2a',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_set_alpha(self):
        input = '[r,t]1a'
        output = sorted([
            'r1a',
            't1a',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_set_multichar(self):
        input = '[ra,tb]1a'
        output = sorted([
            'ra1a',
            'tb1a',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_multiple_ranges(self):
        input = '[r-t]1[a-b]'
        output = sorted([
            'r1a',
            'r1b',
            's1a',
            's1b',
            't1a',
            't1b',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_multiple_sets(self):
        input = '[ra,tb]1[ax,by]'
        output = sorted([
            'ra1ax',
            'ra1by',
            'tb1ax',
            'tb1by',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_set_and_range(self):
        input = '[ra,tb]1[a-c]'
        output = sorted([
            'ra1a',
            'ra1b',
            'ra1c',
            'tb1a',
            'tb1b',
            'tb1c',
        ])

        self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)

    def test_invalid_non_pattern(self):
        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r9a'))

    def test_invalid_range(self):
        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[8-]a'))

        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[-8]a'))

        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[8--9]a'))

    def test_invalid_range_alphanumeric(self):
        self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-a]a')), [])
        self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])

    def test_invalid_range_bounds(self):
        with self.assertRaises(forms.ValidationError):
            sorted(expand_alphanumeric_pattern('r[9-8]a'))
            sorted(expand_alphanumeric_pattern('r[b-a]a'))

    def test_invalid_range_len(self):
        with self.assertRaises(forms.ValidationError):
            sorted(expand_alphanumeric_pattern('r[a-bb]a'))

    def test_invalid_set(self):
        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[a]a'))

        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[a,]a'))

        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[,a]a'))

        with self.assertRaises(ValueError):
            sorted(expand_alphanumeric_pattern('r[a,,b]a'))


class ImportFormTestCase(TestCase):

    def test_format_detection(self):
        form = BulkImportForm()

        data = (
            "a,b,c\n"
            "1,2,3\n"
            "4,5,6\n"
        )
        self.assertEqual(form._detect_format(data), ImportFormatChoices.CSV)

        data = '{"a": 1, "b": 2, "c": 3"}'
        self.assertEqual(form._detect_format(data), ImportFormatChoices.JSON)

        data = '[{"a": 1, "b": 2, "c": 3"}, {"a": 4, "b": 5, "c": 6"}]'
        self.assertEqual(form._detect_format(data), ImportFormatChoices.JSON)

        data = (
            "- a: 1\n"
            "  b: 2\n"
            "  c: 3\n"
            "- a: 4\n"
            "  b: 5\n"
            "  c: 6\n"
        )
        self.assertEqual(form._detect_format(data), ImportFormatChoices.YAML)

        data = (
            "---\n"
            "a: 1\n"
            "b: 2\n"
            "c: 3\n"
            "---\n"
            "a: 4\n"
            "b: 5\n"
            "c: 6\n"
        )
        self.assertEqual(form._detect_format(data), ImportFormatChoices.YAML)

        # Invalid data
        with self.assertRaises(forms.ValidationError):
            form._detect_format('')
        with self.assertRaises(forms.ValidationError):
            form._detect_format('?')

    def test_csv_delimiters(self):
        form = BulkImportForm()

        data = (
            "a,b,c\n"
            "1,2,3\n"
            "4,5,6\n"
        )
        self.assertEqual(form._clean_csv(data, delimiter=','), [
            {'a': '1', 'b': '2', 'c': '3'},
            {'a': '4', 'b': '5', 'c': '6'},
        ])

        data = (
            "a;b;c\n"
            "1;2;3\n"
            "4;5;6\n"
        )
        self.assertEqual(form._clean_csv(data, delimiter=';'), [
            {'a': '1', 'b': '2', 'c': '3'},
            {'a': '4', 'b': '5', 'c': '6'},
        ])

        data = (
            "a\tb\tc\n"
            "1\t2\t3\n"
            "4\t5\t6\n"
        )
        self.assertEqual(form._clean_csv(data, delimiter='\t'), [
            {'a': '1', 'b': '2', 'c': '3'},
            {'a': '4', 'b': '5', 'c': '6'},
        ])


class BulkRenameFormTestCase(TestCase):
    def test_no_strip_whitespace(self):
        # Tests to make sure Bulk Rename Form isn't stripping whitespaces
        # See: https://github.com/netbox-community/netbox/issues/13791
        form = BulkRenameForm(data={
            "find": " hello ",
            "replace": " world "
        })
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["find"], " hello ")
        self.assertEqual(form.cleaned_data["replace"], " world ")


class GetFieldValueTestCase(TestCase):

    @classmethod
    def setUpTestData(cls):
        class TestForm(forms.Form):
            site = forms.ModelChoiceField(
                queryset=Site.objects.all(),
                required=False
            )
        cls.form_class = TestForm

        cls.sites = (
            Site(name='Test Site 1', slug='test-site-1'),
            Site(name='Test Site 2', slug='test-site-2'),
        )
        Site.objects.bulk_create(cls.sites)

    def test_unbound_without_initial(self):
        form = self.form_class()
        self.assertEqual(
            get_field_value(form, 'site'),
            None
        )

    def test_unbound_with_initial(self):
        form = self.form_class(initial={'site': self.sites[0].pk})
        self.assertEqual(
            get_field_value(form, 'site'),
            self.sites[0].pk
        )

    def test_bound_value_without_initial(self):
        form = self.form_class({'site': self.sites[0].pk})
        self.assertEqual(
            get_field_value(form, 'site'),
            self.sites[0].pk
        )

    def test_bound_value_with_initial(self):
        form = self.form_class({'site': self.sites[0].pk}, initial={'site': self.sites[1].pk})
        self.assertEqual(
            get_field_value(form, 'site'),
            self.sites[0].pk
        )

    def test_bound_null_without_initial(self):
        form = self.form_class({'site': None})
        self.assertEqual(
            get_field_value(form, 'site'),
            None
        )

    def test_bound_null_with_initial(self):
        form = self.form_class({'site': None}, initial={'site': self.sites[1].pk})
        self.assertEqual(
            get_field_value(form, 'site'),
            None
        )


class CSVSelectWidgetTestCase(TestCase):
    """
    Validate that CSVSelectWidget treats blank values as omitted.
    This allows model defaults to be applied when CSV fields are present but empty.
    Related to issue #20645.
    """

    def test_blank_value_treated_as_omitted(self):
        """Test that blank string values are treated as omitted"""
        widget = CSVSelectWidget()
        data = {'test_field': ''}
        self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))

    def test_none_value_treated_as_omitted(self):
        """Test that None values are treated as omitted"""
        widget = CSVSelectWidget()
        data = {'test_field': None}
        self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))

    def test_missing_field_treated_as_omitted(self):
        """Test that missing fields are treated as omitted"""
        widget = CSVSelectWidget()
        data = {}
        self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))

    def test_valid_value_not_omitted(self):
        """Test that valid values are not treated as omitted"""
        widget = CSVSelectWidget()
        data = {'test_field': 'valid_value'}
        self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))


class SelectMultipleWidgetTestCase(TestCase):
    """
    Validate filtering behavior of AvailableOptions and SelectedOptions widgets.
    """

    def test_available_options_flat_choices(self):
        """AvailableOptions should exclude selected values from flat choices"""
        widget = AvailableOptions(choices=[
            (1, 'Option 1'),
            (2, 'Option 2'),
            (3, 'Option 3'),
        ])
        widget.optgroups('test', ['2'], None)

        self.assertEqual(len(widget.choices), 2)
        self.assertEqual(widget.choices[0], (1, 'Option 1'))
        self.assertEqual(widget.choices[1], (3, 'Option 3'))

    def test_available_options_optgroups(self):
        """AvailableOptions should exclude selected values from optgroups"""
        widget = AvailableOptions(choices=[
            ('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
            ('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
        ])

        # Select options 2 and 3
        widget.optgroups('test', ['2', '3'], None)

        # Should have 2 groups with filtered choices
        self.assertEqual(len(widget.choices), 2)
        self.assertEqual(widget.choices[0][0], 'Group A')
        self.assertEqual(widget.choices[0][1], [(1, 'Option 1')])
        self.assertEqual(widget.choices[1][0], 'Group B')
        self.assertEqual(widget.choices[1][1], [(4, 'Option 4')])

    def test_selected_options_flat_choices(self):
        """SelectedOptions should include only selected values from flat choices"""
        widget = SelectedOptions(choices=[
            (1, 'Option 1'),
            (2, 'Option 2'),
            (3, 'Option 3'),
        ])

        # Select option 2
        widget.optgroups('test', ['2'], None)

        # Should only have option 2
        self.assertEqual(len(widget.choices), 1)
        self.assertEqual(widget.choices[0], (2, 'Option 2'))

    def test_selected_options_optgroups(self):
        """SelectedOptions should include only selected values from optgroups"""
        widget = SelectedOptions(choices=[
            ('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
            ('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
        ])

        # Select options 2 and 3
        widget.optgroups('test', ['2', '3'], None)

        # Should have 2 groups with only selected choices
        self.assertEqual(len(widget.choices), 2)
        self.assertEqual(widget.choices[0][0], 'Group A')
        self.assertEqual(widget.choices[0][1], [(2, 'Option 2')])
        self.assertEqual(widget.choices[1][0], 'Group B')
        self.assertEqual(widget.choices[1][1], [(3, 'Option 3')])


class DynamicChoiceFieldTestCase(TestCase):
    """
    Validate that DynamicChoiceField.get_bound_field() limits choices to the current
    selection and clears them when nothing is selected.
    """
    CHOICES = [('a', 'Option A'), ('b', 'Option B'), ('c', 'Option C')]

    def _make_form(self, data=None):
        class TestForm(forms.Form):
            field = DynamicChoiceField(choices=self.CHOICES, required=False)
        return TestForm(data=data)

    def test_unbound_clears_choices(self):
        form = self._make_form()
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [])

    def test_bound_with_value_filters_to_selection(self):
        form = self._make_form(data={'field': 'b'})
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [('b', 'Option B')])

    def test_bound_with_no_value_clears_choices(self):
        form = self._make_form(data={})
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [])


class DynamicMultipleChoiceFieldTestCase(TestCase):
    """
    Validate that DynamicMultipleChoiceField.get_bound_field() limits choices to
    the current selection and clears them when nothing is selected.
    """
    CHOICES = [('a', 'Option A'), ('b', 'Option B'), ('c', 'Option C')]

    def _make_form(self, data=None):
        class TestForm(forms.Form):
            field = DynamicMultipleChoiceField(choices=self.CHOICES, required=False)
        return TestForm(data=data)

    def test_unbound_clears_choices(self):
        """Regression test for #22328: unbound form must not retain the full choices list."""
        form = self._make_form()
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [])

    def test_bound_with_values_filters_to_selection(self):
        form = self._make_form(data={'field': ['a', 'c']})
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [('a', 'Option A'), ('c', 'Option C')])

    def test_bound_with_no_values_clears_choices(self):
        form = self._make_form(data={})
        form.fields['field'].get_bound_field(form, 'field')
        self.assertEqual(form.fields['field'].choices, [])


class GetCapacityUnitLabelTestCase(TestCase):
    """
    Test the get_capacity_unit_label function for correct base unit label.
    """

    def test_si_label(self):
        self.assertEqual(get_capacity_unit_label(1000), 'MB')

    def test_iec_label(self):
        self.assertEqual(get_capacity_unit_label(1024), 'MiB')
