# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
import json
import unittest

from libcloud.dns.base import Zone, Record
from libcloud.dns.types import (
    RecordType,
    RecordError,
    ZoneDoesNotExistError,
    ZoneAlreadyExistsError,
    RecordDoesNotExistError,
    RecordAlreadyExistsError,
)
from libcloud.utils.py3 import httplib
from libcloud.test.secrets import DNS_GANDI_LIVE
from libcloud.common.gandi_live import JsonParseError, GandiLiveBaseError, InvalidRequestError
from libcloud.test.file_fixtures import DNSFileFixtures
from libcloud.dns.drivers.gandi_live import GandiLiveDNSDriver
from libcloud.test.common.test_gandi_live import BaseGandiLiveMockHttp


class GandiLiveTests(unittest.TestCase):
    def setUp(self):
        GandiLiveDNSDriver.connectionCls.conn_class = GandiLiveMockHttp
        GandiLiveMockHttp.type = None
        self.driver = GandiLiveDNSDriver(*DNS_GANDI_LIVE)
        self.test_zone = Zone(
            id="example.com",
            type="master",
            ttl=None,
            domain="example.com",
            extra={"zone_uuid": "a53re"},
            driver=self,
        )
        self.test_bad_zone = Zone(
            id="badexample.com",
            type="master",
            ttl=None,
            domain="badexample.com",
            extra={"zone_uuid": "a53rf"},
            driver=self,
        )
        self.test_record = Record(
            id="A:bob",
            type=RecordType.A,
            name="bob",
            zone=self.test_zone,
            data="127.0.0.1",
            driver=self,
            extra={},
        )
        self.test_bad_record = Record(
            id="A:jane",
            type=RecordType.A,
            name="jane",
            zone=self.test_bad_zone,
            data="127.0.0.1",
            driver=self,
            extra={},
        )

    def test_list_zones(self):
        zones = self.driver.list_zones()
        self.assertEqual(len(zones), 2)
        zone = zones[0]
        self.assertEqual(zone.id, "example.com")
        self.assertEqual(zone.type, "master")
        self.assertEqual(zone.domain, "example.com")
        self.assertIsNone(zone.ttl)
        zone = zones[1]
        self.assertEqual(zone.id, "example.net")
        self.assertEqual(zone.type, "master")
        self.assertEqual(zone.domain, "example.net")
        self.assertIsNone(zone.ttl)

    def test_create_zone(self):
        zone = self.driver.create_zone("example.org", extra={"name": "Example"})
        self.assertEqual(zone.id, "example.org")
        self.assertEqual(zone.domain, "example.org")
        self.assertEqual(zone.extra["zone_uuid"], "54321")

    def test_create_zone_without_name(self):
        zone = self.driver.create_zone("example.org")
        self.assertEqual(zone.id, "example.org")
        self.assertEqual(zone.domain, "example.org")
        self.assertEqual(zone.extra["zone_uuid"], "54321")

    def test_get_zone(self):
        zone = self.driver.get_zone("example.com")
        self.assertEqual(zone.id, "example.com")
        self.assertEqual(zone.type, "master")
        self.assertEqual(zone.domain, "example.com")
        self.assertIsNone(zone.ttl)

    def test_list_records(self):
        records = self.driver.list_records(self.test_zone)
        self.assertEqual(len(records), 3)
        record = records[0]
        self.assertEqual(record.id, "A:@")
        self.assertEqual(record.name, "@")
        self.assertEqual(record.type, RecordType.A)
        self.assertEqual(record.data, "127.0.0.1")
        record = records[1]
        self.assertEqual(record.id, "CNAME:www")
        self.assertEqual(record.name, "www")
        self.assertEqual(record.type, RecordType.CNAME)
        self.assertEqual(record.data, "bob.example.com.")
        record = records[2]
        self.assertEqual(record.id, "A:bob")
        self.assertEqual(record.name, "bob")
        self.assertEqual(record.type, RecordType.A)
        self.assertEqual(record.data, "127.0.1.1")

    def test_get_record(self):
        record = self.driver.get_record(self.test_zone.id, "A:bob")
        self.assertEqual(record.id, "A:bob")
        self.assertEqual(record.name, "bob")
        self.assertEqual(record.type, RecordType.A)
        self.assertEqual(record.data, "127.0.1.1")

    def test_create_record(self):
        record = self.driver.create_record(
            "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 400}
        )
        self.assertEqual(record.id, "AAAA:alice")
        self.assertEqual(record.name, "alice")
        self.assertEqual(record.type, RecordType.AAAA)
        self.assertEqual(record.data, "::1")

    def test_create_record_doesnt_throw_if_ttl_is_not_provided(self):
        record = self.driver.create_record("alice", self.test_zone, "AAAA", "::1")
        self.assertEqual(record.id, "AAAA:alice")
        self.assertEqual(record.name, "alice")
        self.assertEqual(record.type, RecordType.AAAA)
        self.assertEqual(record.data, "::1")

    def test_bad_record_validation(self):
        with self.assertRaises(RecordError) as ctx:
            self.driver.create_record(
                "alice", self.test_zone, "AAAA", "1" * 1025, extra={"ttl": 400}
            )
        self.assertTrue("Record data must be" in str(ctx.exception))
        with self.assertRaises(RecordError) as ctx:
            self.driver.create_record("alice", self.test_zone, "AAAA", "::1", extra={"ttl": 10})
        self.assertTrue("TTL must be at least" in str(ctx.exception))
        with self.assertRaises(RecordError) as ctx:
            self.driver.create_record(
                "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 31 * 24 * 60 * 60}
            )
        self.assertTrue("TTL must not exceed" in str(ctx.exception))

    def test_update_record(self):
        record = self.driver.update_record(
            self.test_record, "bob", RecordType.A, "192.168.0.2", {"ttl": 500}
        )
        self.assertEqual(record.id, "A:bob")
        self.assertEqual(record.name, "bob")
        self.assertEqual(record.type, RecordType.A)
        self.assertEqual(record.data, "192.168.0.2")

    def test_delete_record(self):
        success = self.driver.delete_record(self.test_record)
        self.assertTrue(success)

    def test_export_bind(self):
        bind_export = self.driver.export_zone_to_bind_format(self.test_zone)
        bind_lines = bind_export.decode("utf8").split("\n")
        self.assertEqual(bind_lines[0], "@ 10800 IN A 127.0.0.1")

    def test_bad_json_response(self):
        with self.assertRaises(JsonParseError):
            self.driver.get_zone("badexample.com")

    def test_no_record_found(self):
        with self.assertRaises(RecordDoesNotExistError):
            self.driver.get_record(self.test_zone.id, "A:none")

    def test_record_already_exists(self):
        with self.assertRaises(RecordAlreadyExistsError):
            self.driver.create_record(
                "bob", self.test_bad_zone, "A", "127.0.0.1", extra={"ttl": 400}
            )

    def test_no_zone_found(self):
        with self.assertRaises(ZoneDoesNotExistError):
            self.driver.get_zone("nosuchzone.com")

    def test_zone_already_exists(self):
        with self.assertRaises(ZoneAlreadyExistsError):
            self.driver.create_zone("badexample.com")

    # This is somewhat spurious - the driver should be preventing any invalid
    # request error from arising.
    def test_suberrors(self):
        with self.assertRaises(InvalidRequestError) as ctx:
            self.driver.update_record(
                self.test_bad_record, "jane", RecordType.A, "192.168.0.2", {"ttl": 500}
            )
        self.assertTrue("is not a foo" in str(ctx.exception))

    def test_other_error(self):
        with self.assertRaises(GandiLiveBaseError):
            self.driver.list_records(self.test_bad_zone)

    def test_mx_record(self):
        record = self.driver.get_record(self.test_zone.id, "MX:lists")
        self.assertEqual(record.extra["priority"], "10")
        self.assertTrue("_other_records" in record.extra)
        other_record = record.extra["_other_records"][0]
        self.assertEqual(other_record["extra"]["priority"], "20")

    def test_ex_create_multivalue_record(self):
        records = self.driver.ex_create_multi_value_record(
            "alice", self.test_zone, "AAAA", ["::1", "::2"], extra={"ttl": 400}
        )
        self.assertEqual(records[0].id, "AAAA:alice")
        self.assertEqual(records[0].name, "alice")
        self.assertEqual(records[0].type, RecordType.AAAA)
        self.assertEqual(records[0].data, "::1")
        self.assertEqual(records[1].id, "AAAA:alice")
        self.assertEqual(records[1].name, "alice")
        self.assertEqual(records[1].type, RecordType.AAAA)
        self.assertEqual(records[1].data, "::2")

    def test_update_multivalue_record(self):
        record = self.driver.get_record(self.test_zone.id, "MX:lists")
        updated = self.driver.update_record(
            record, None, None, "mail1", {"ttl": 400, "priority": 10}
        )
        self.assertEqual(updated.extra["priority"], "10")
        self.assertEqual(updated.data, "mail1")
        self.assertTrue("_other_records" in record.extra)
        other_record = record.extra["_other_records"][0]
        self.assertEqual(other_record["extra"]["priority"], "20")

    def test_ex_update_gandi_zone_name(self):
        updated = self.driver.ex_update_gandi_zone_name("111111", "Foo")
        self.assertTrue(updated)

    def test_ex_delete_gandi_zone(self):
        deleted = self.driver.ex_delete_gandi_zone("111111")
        self.assertTrue(deleted)


class GandiLiveMockHttp(BaseGandiLiveMockHttp):
    fixtures = DNSFileFixtures("gandi_live")

    def _json_api_v5_domains_get(self, method, url, body, headers):
        body = self.fixtures.load("list_zones.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_get(self, method, url, body, headers):
        body = self.fixtures.load("get_zone.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_badexample_com_get(self, method, url, body, headers):
        body = self.fixtures.load("get_bad_zone.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_nosuchzone_com_get(self, method, url, body, headers):
        body = self.fixtures.load("get_nonexistent_zone.json")
        return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND])

    def _json_api_v5_zones_post(self, method, url, body, headers):
        input = json.loads(body)
        if "badexample" in input["name"]:
            body = self.fixtures.load("create_bad_zone.json")
            return (httplib.CONFLICT, body, {}, httplib.responses[httplib.CONFLICT])
        else:
            body = self.fixtures.load("create_zone.json")
            return (
                httplib.OK,
                body,
                {"Location": "/zones/54321"},
                httplib.responses[httplib.OK],
            )

    def _json_api_v5_zones_111111_patch(self, method, url, body, headers):
        body = self.fixtures.load("update_gandi_zone.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_zones_111111_delete(self, method, url, body, headers):
        return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_org_patch(self, method, url, body, headers):
        body = self.fixtures.load("create_domain.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_get(self, method, url, body, headers):
        body = self.fixtures.load("list_records.json")
        resp_headers = {}
        if headers is not None and "Accept" in headers and headers["Accept"] == "text/plain":
            body = self.fixtures.load("list_records_bind.txt")
            resp_headers["Content-Type"] = "text/plain"
        return (httplib.OK, body, resp_headers, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_bob_A_get(self, method, url, body, headers):
        body = self.fixtures.load("get_record.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_none_A_get(self, method, url, body, headers):
        body = self.fixtures.load("get_nonexistent_record.json")
        return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND])

    def _json_api_v5_domains_example_com_records_lists_MX_get(self, method, url, body, headers):
        body = self.fixtures.load("get_mx_record.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_lists_MX_put(self, method, url, body, headers):
        body = self.fixtures.load("update_mx_record.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_post(self, method, url, body, headers):
        body = self.fixtures.load("create_record.json")
        return (
            httplib.OK,
            body,
            {"Location": "/zones/12345/records/alice/AAAA"},
            httplib.responses[httplib.OK],
        )

    def _json_api_v5_domains_badexample_com_records_post(self, method, url, body, headers):
        body = self.fixtures.load("create_existing_record.json")
        return (httplib.CONFLICT, body, {}, httplib.responses[httplib.CONFLICT])

    def _json_api_v5_domains_badexample_com_records_get(self, method, url, body, headers):
        return (
            httplib.INTERNAL_SERVER_ERROR,
            body,
            {},
            httplib.responses[httplib.INTERNAL_SERVER_ERROR],
        )

    def _json_api_v5_domains_badexample_com_records_jane_A_put(self, method, url, body, headers):
        body = self.fixtures.load("update_bad_record.json")
        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.BAD_REQUEST])

    def _json_api_v5_domains_example_com_records_bob_A_put(self, method, url, body, headers):
        body = self.fixtures.load("update_record.json")
        return (httplib.OK, body, {}, httplib.responses[httplib.OK])

    def _json_api_v5_domains_example_com_records_bob_A_delete(self, method, url, body, headers):
        return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.OK])


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