#
#
#

from unittest import TestCase

from helpers import SimpleProvider

from octodns.record import Record
from octodns.record.exception import ValidationError
from octodns.record.rr import RrParseError
from octodns.record.svcb import SvcbRecord, SvcbValue
from octodns.zone import Zone


class TestRecordSvcb(TestCase):
    zone = Zone('unit.tests.', [])

    def test_svcb(self):
        aliasmode_value = SvcbValue(
            {'svcpriority': 0, 'targetname': 'foo.example.com.'}
        )
        aliasmode_data = {'ttl': 300, 'value': aliasmode_value}
        a = SvcbRecord(self.zone, 'alias', aliasmode_data)
        self.assertEqual('alias', a.name)
        self.assertEqual('alias.unit.tests.', a.fqdn)
        self.assertEqual(300, a.ttl)
        self.assertEqual(
            aliasmode_value['svcpriority'], a.values[0].svcpriority
        )
        self.assertEqual(aliasmode_value['targetname'], a.values[0].targetname)
        self.assertEqual(aliasmode_value['svcparams'], a.values[0].svcparams)
        self.assertEqual(aliasmode_data, a.data)

        servicemode_values = [
            SvcbValue(
                {
                    'svcpriority': 1,
                    'targetname': 'foo.example.com.',
                    'svcparams': {'port': 8002},
                }
            ),
            SvcbValue(
                {
                    'svcpriority': 2,
                    'targetname': 'foo.example.net.',
                    'svcparams': {'port': 8080},
                }
            ),
        ]
        servicemode_data = {'ttl': 300, 'values': servicemode_values}
        b = SvcbRecord(self.zone, 'service', servicemode_data)
        self.assertEqual('service', b.name)
        self.assertEqual('service.unit.tests.', b.fqdn)
        self.assertEqual(300, b.ttl)
        self.assertEqual(
            servicemode_values[0]['svcpriority'], b.values[0].svcpriority
        )
        self.assertEqual(
            servicemode_values[0]['targetname'], b.values[0].targetname
        )
        self.assertEqual(
            servicemode_values[0]['svcparams'], b.values[0].svcparams
        )
        self.assertEqual(
            servicemode_values[1]['svcpriority'], b.values[1].svcpriority
        )
        self.assertEqual(
            servicemode_values[1]['targetname'], b.values[1].targetname
        )
        self.assertEqual(
            servicemode_values[1]['svcparams'], b.values[1].svcparams
        )
        self.assertEqual(servicemode_data, b.data)

        target = SimpleProvider()
        # No changes with self
        self.assertFalse(b.changes(b, target))
        # Diff in priority causes change
        other = SvcbRecord(
            self.zone, 'service2', {'ttl': 30, 'values': servicemode_values}
        )
        other.values[0].svcpriority = 22
        change = b.changes(other, target)
        self.assertEqual(change.existing, b)
        self.assertEqual(change.new, other)
        # Diff in target causes change
        other.values[0].svcpriority = b.values[0].svcpriority
        other.values[0].targetname = 'blabla.example.com.'
        change = b.changes(other, target)
        self.assertEqual(change.existing, b)
        self.assertEqual(change.new, other)
        # Diff in params causes change
        other.values[0].targetname = b.values[0].targetname
        other.values[0].svcparams = {'port': '8888'}
        change = b.changes(other, target)
        self.assertEqual(change.existing, b)
        self.assertEqual(change.new, other)

        # __repr__ doesn't blow up
        a.__repr__()
        b.__repr__()

    def test_svcb_value_rdata_text(self):
        # empty string won't parse
        with self.assertRaises(RrParseError):
            SvcbValue.parse_rdata_text('')

        # single word won't parse
        with self.assertRaises(RrParseError):
            SvcbValue.parse_rdata_text('nope')

        # Double keys are not allowed
        with self.assertRaises(RrParseError):
            SvcbValue.parse_rdata_text('1 foo.example.com port=8080 port=8084')

        # priority not int
        self.assertEqual(
            {
                'svcpriority': 'one',
                'targetname': 'foo.example.com',
                'svcparams': dict(),
            },
            SvcbValue.parse_rdata_text('one foo.example.com'),
        )

        # valid with params
        self.assertEqual(
            {
                'svcpriority': 1,
                'targetname': 'svcb.unit.tests.',
                'svcparams': {'port': '8080', 'no-default-alpn': None},
            },
            SvcbValue.parse_rdata_text(
                '1 svcb.unit.tests. port=8080 no-default-alpn'
            ),
        )

        # quoted target
        self.assertEqual(
            {
                'svcpriority': 1,
                'targetname': 'svcb.unit.tests.',
                'svcparams': dict(),
            },
            SvcbValue.parse_rdata_text('1 "svcb.unit.tests."'),
        )

        zone = Zone('unit.tests.', [])
        a = SvcbRecord(
            zone,
            'svc',
            {
                'ttl': 32,
                'value': {
                    'svcpriority': 1,
                    'targetname': 'svcb.unit.tests.',
                    'svcparams': {'port': '8080'},
                },
            },
        )
        self.assertEqual(1, a.values[0].svcpriority)
        self.assertEqual('svcb.unit.tests.', a.values[0].targetname)
        self.assertEqual({'port': '8080'}, a.values[0].svcparams)

        # both directions should match
        rdata = '1 svcb.unit.tests. no-default-alpn port=8080 ipv4hint=192.0.2.2,192.0.2.53 key3333=foobar'
        record = SvcbRecord(
            zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)}
        )
        self.assertEqual(rdata, record.values[0].rdata_text)

        # both directions should match
        rdata = '0 svcb.unit.tests.'
        record = SvcbRecord(
            zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)}
        )
        self.assertEqual(rdata, record.values[0].rdata_text)

        # quoted params need to be correctly handled
        rdata = '1 svcb.unit.tests. no-default-alpn port=8080 ipv4hint="192.0.2.2,192.0.2.53" key3333="foobar"'
        record = SvcbRecord(
            zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)}
        )
        self.assertEqual(rdata.replace('"', ''), record.values[0].rdata_text)

    def test_svcb_value(self):
        a = SvcbValue(
            {'svcpriority': 0, 'targetname': 'foo.', 'svcparams': dict()}
        )
        b = SvcbValue(
            {'svcpriority': 1, 'targetname': 'foo.', 'svcparams': dict()}
        )
        c = SvcbValue(
            {
                'svcpriority': 0,
                'targetname': 'foo.',
                'svcparams': {'port': 8080, 'no-default-alpn': None},
            }
        )
        d = SvcbValue(
            {
                'svcpriority': 0,
                'targetname': 'foo.',
                'svcparams': {'alpn': ['h2', 'h3'], 'port': 8080},
            }
        )
        e = SvcbValue(
            {
                'svcpriority': 0,
                'targetname': 'mmm.',
                'svcparams': {'ipv4hint': ['192.0.2.1']},
            }
        )

        self.assertEqual(a, a)
        self.assertEqual(b, b)
        self.assertEqual(c, c)
        self.assertEqual(d, d)
        self.assertEqual(e, e)

        self.assertNotEqual(a, b)
        self.assertNotEqual(a, c)
        self.assertNotEqual(a, d)
        self.assertNotEqual(a, e)
        self.assertNotEqual(b, a)
        self.assertNotEqual(b, c)
        self.assertNotEqual(b, d)
        self.assertNotEqual(b, e)
        self.assertNotEqual(c, a)
        self.assertNotEqual(c, b)
        self.assertNotEqual(c, d)
        self.assertNotEqual(c, e)
        self.assertNotEqual(d, a)
        self.assertNotEqual(d, b)
        self.assertNotEqual(d, c)
        self.assertNotEqual(d, e)
        self.assertNotEqual(e, a)
        self.assertNotEqual(e, b)
        self.assertNotEqual(e, c)
        self.assertNotEqual(e, d)

        self.assertTrue(a < b)
        self.assertTrue(a < c)

        self.assertTrue(b > a)
        self.assertTrue(b > c)

        self.assertTrue(c > a)
        self.assertTrue(c < b)

        self.assertTrue(a <= b)
        self.assertTrue(a <= c)
        self.assertTrue(a <= a)
        self.assertTrue(a >= a)

        self.assertTrue(b >= a)
        self.assertTrue(b >= c)
        self.assertTrue(b >= b)
        self.assertTrue(b <= b)

        self.assertTrue(c >= a)
        self.assertTrue(c <= b)
        self.assertTrue(c >= c)
        self.assertTrue(c <= c)

        # Hash
        values = set()
        values.add(a)
        self.assertTrue(a in values)
        self.assertFalse(b in values)
        values.add(b)
        self.assertTrue(b in values)

    def test_validation(self):
        # doesn't blow up
        Record.new(
            self.zone,
            'svcb',
            {
                'type': 'SVCB',
                'ttl': 600,
                'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz.'},
            },
        )

        # Wildcards are fine
        Record.new(
            self.zone,
            '*',
            {
                'type': 'SVCB',
                'ttl': 600,
                'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz.'},
            },
        )

        # missing priority
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {'targetname': 'foo.bar.baz.'},
                },
            )
        self.assertEqual(['missing svcpriority'], ctx.exception.reasons)

        # invalid priority
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 'foo',
                        'targetname': 'foo.bar.baz.',
                    },
                },
            )
        self.assertEqual(['invalid priority "foo"'], ctx.exception.reasons)

        # invalid priority (out of range)
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 100000,
                        'targetname': 'foo.bar.baz.',
                    },
                },
            )
        self.assertEqual(['invalid priority "100000"'], ctx.exception.reasons)

        # missing target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1}},
            )
        self.assertEqual(['missing targetname'], ctx.exception.reasons)

        # invalid target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz'},
                },
            )
        self.assertEqual(
            ['SVCB value "foo.bar.baz" missing trailing .'],
            ctx.exception.reasons,
        )

        # falsey target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {'svcpriority': 1, 'targetname': ''},
                },
            )
        self.assertEqual(['missing targetname'], ctx.exception.reasons)

        # target must be a valid FQDN
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'bla foo.bar.com.',
                    },
                },
            )
        self.assertEqual(
            ['Invalid SVCB target "bla foo.bar.com." is not a valid FQDN.'],
            ctx.exception.reasons,
        )

        # target can be root label
        Record.new(
            self.zone,
            'foo',
            {
                'type': 'SVCB',
                'ttl': 600,
                'value': {'svcpriority': 1, 'targetname': '.'},
            },
        )

        # Params can't be set for AliasMode
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 0,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'port': '8000'},
                    },
                },
            )
        self.assertEqual(
            ['svcparams set on AliasMode SVCB record'], ctx.exception.reasons
        )

        # Unknown param
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'blablabla': '222'},
                    },
                },
            )
        self.assertEqual(['Unknown SvcParam blablabla'], ctx.exception.reasons)

        # Port number invalid
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'port': 100000},
                    },
                },
            )
        self.assertEqual(
            ['port 100000 is not a valid number'], ctx.exception.reasons
        )

        # Port number not an int
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'port': 'foo'},
                    },
                },
            )
        self.assertEqual(['port is not a number'], ctx.exception.reasons)

        # no-default-alpn set
        Record.new(
            self.zone,
            'foo',
            {
                'type': 'SVCB',
                'ttl': 600,
                'value': {
                    'svcpriority': 1,
                    'targetname': 'foo.bar.com.',
                    'svcparams': {'no-default-alpn': None},
                },
            },
        )

        # no-default-alpn has value
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'no-default-alpn': 'foobar'},
                    },
                },
            )
        self.assertEqual(
            ['SvcParam no-default-alpn has value when it should not'],
            ctx.exception.reasons,
        )

        # alpn is broken
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'alpn': ['h2', '😅']},
                    },
                },
            )
        self.assertEqual(['non ASCII character in "😅"'], ctx.exception.reasons)

        # svcbvaluelist that is not a list
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {
                            'ipv4hint': '192.0.2.1,192.0.2.2',
                            'ipv6hint': '2001:db8::1',
                            'mandatory': 'ipv6hint',
                            'alpn': 'h2,h3',
                        },
                    },
                },
            )
        self.assertEqual(
            [
                'ipv4hint is not a list',
                'ipv6hint is not a list',
                'mandatory is not a list',
                'alpn is not a list',
            ],
            ctx.exception.reasons,
        )

        # ipv4hint
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {
                            'ipv4hint': ['192.0.2.0', '500.500.30.30']
                        },
                    },
                },
            )
        self.assertEqual(
            ['ipv4hint "500.500.30.30" is not a valid IPv4 address'],
            ctx.exception.reasons,
        )

        # ipv6hint
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {
                            'ipv6hint': ['2001:db8:43::1', 'notanip']
                        },
                    },
                },
            )
        self.assertEqual(
            ['ipv6hint "notanip" is not a valid IPv6 address'],
            ctx.exception.reasons,
        )

        # mandatory
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {
                            'mandatory': ['ipv4hint', 'unknown', 'key4444']
                        },
                    },
                },
            )
        self.assertEqual(
            ['unsupported SvcParam "unknown" in mandatory'],
            ctx.exception.reasons,
        )

        # ech
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {'ech': ' dG90YWxseUZha2VFQ0hPcHRpb24'},
                    },
                },
            )
        self.assertEqual(
            ['ech SvcParam is invalid Base64'], ctx.exception.reasons
        )

        # broken keyNNNN format
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone,
                'foo',
                {
                    'type': 'SVCB',
                    'ttl': 600,
                    'value': {
                        'svcpriority': 1,
                        'targetname': 'foo.bar.com.',
                        'svcparams': {
                            'key100000': 'foo',
                            'key3333': 'bar',
                            'keyXXX': 'foo',
                        },
                    },
                },
            )
        self.assertEqual(
            [
                'SvcParam key "key100000" has wrong key number',
                'SvcParam key "keyXXX" has wrong format',
            ],
            ctx.exception.reasons,
        )


class TestSrvValue(TestCase):

    def test_template(self):
        value = SvcbValue({'svcpriority': 0, 'targetname': 'foo.example.com.'})
        got = value.template({'needle': 42})
        self.assertIs(value, got)

        value = SvcbValue(
            {'svcpriority': 0, 'targetname': 'foo.{needle}.example.com.'}
        )
        got = value.template({'needle': 42})
        self.assertIsNot(value, got)
        self.assertEqual('foo.42.example.com.', got.targetname)
