/*
Copyright 2020 The Kubernetes Authors.

Licensed 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.
*/
package civo

import (
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/civo/civogo"
	"github.com/google/go-cmp/cmp"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sigs.k8s.io/external-dns/endpoint"
	"sigs.k8s.io/external-dns/plan"
)

func TestNewProvider(t *testing.T) {
	t.Setenv("CIVO_TOKEN", "xxxxxxxxxxxxxxx")
	_, err := newProvider(endpoint.NewDomainFilter([]string{"test.civo.com"}), true)
	require.NoError(t, err)

	_ = os.Unsetenv("CIVO_TOKEN")
}

func TestNewCivoProviderNoToken(t *testing.T) {
	_, err := newProvider(endpoint.NewDomainFilter([]string{"test.civo.com"}), true)
	assert.Error(t, err)

	assert.Equal(t, "no token found", err.Error())
}

func TestCivoProviderZones(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns": `[
			{"id": "12345", "account_id": "1", "name": "example.com"},
			{"id": "12346", "account_id": "1", "name": "example.net"}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client: *client,
	}

	expected, err := client.ListDNSDomains()
	assert.NoError(t, err)

	zones, err := provider.Zones(t.Context())
	assert.NoError(t, err)

	// Check if the return is a DNSDomain type
	assert.ElementsMatch(t, zones, expected)
}

func TestCivoProviderZonesWithError(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns-error": `[]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client: *client,
	}

	_, err := provider.Zones(t.Context())
	assert.Error(t, err)
}

func TestCivoProviderRecords(t *testing.T) {
	client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{
		{
			Method: "GET",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody: ``,
					URL:         "/v2/dns/12345/records",
					ResponseBody: `[
						{"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600},
						{"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600}
						]`,
				},
				{
					RequestBody: ``,
					URL:         "/v2/dns",
					ResponseBody: `[
						{"id": "12345", "account_id": "1", "name": "example.com"},
						{"id": "12346", "account_id": "1", "name": "example.net"}
						]`,
				},
			},
		},
	})

	defer server.Close()
	provider := &CivoProvider{
		Client:       *client,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	expected, err := client.ListDNSRecords("12345")
	assert.NoError(t, err)

	records, err := provider.Records(t.Context())
	assert.NoError(t, err)

	assert.Equal(t, strings.TrimSuffix(records[0].DNSName, ".example.com"), expected[0].Name)
	assert.Equal(t, records[0].RecordType, string(expected[0].Type))
	assert.Equal(t, int(records[0].RecordTTL), expected[0].TTL)

	assert.Equal(t, strings.TrimSuffix(records[1].DNSName, ".example.com"), expected[1].Name)
	assert.Equal(t, records[1].RecordType, string(expected[1].Type))
	assert.Equal(t, int(records[1].RecordTTL), expected[1].TTL)
}

func TestCivoProviderRecordsWithError(t *testing.T) {
	client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{
		{
			Method: "GET",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody: ``,
					URL:         "/v2/dns/12345/records",
					ResponseBody: `[
						{"id": "1", "domain_id":"12345", "account_id": "1", "name": "", "type": "A", "value": "10.0.0.0", "ttl": 600},
						{"id": "2", "account_id": "1", "domain_id":"12345", "name": "", "type": "A", "value": "10.0.0.1", "ttl": 600}
						]`,
				},
				{
					RequestBody:  ``,
					URL:          "/v2/dns",
					ResponseBody: `invalid-json-data`,
				},
			},
		},
	})

	defer server.Close()

	provider := &CivoProvider{
		Client:       *client,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	_, err := client.ListDNSRecords("12345")
	assert.NoError(t, err)

	endpoint, err := provider.Records(t.Context())
	assert.Error(t, err)
	assert.Nil(t, endpoint)

}

func TestCivoProviderWithoutRecords(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns/12345/records": `[]`,
		"/v2/dns": `[
			{"id": "12345", "account_id": "1", "name": "example.com"},
			{"id": "12346", "account_id": "1", "name": "example.net"}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client:       *client,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	records, err := provider.Records(t.Context())
	assert.NoError(t, err)

	assert.Empty(t, records)
}

func TestCivoProcessCreateActionsLogs(t *testing.T) {
	log.SetOutput(io.Discard)
	t.Run("Logs Skipping Zone, no creates found", func(t *testing.T) {
		zonesByID := map[string]civogo.DNSDomain{
			"example.com": {
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
		}

		recordsByZoneID := map[string][]civogo.DNSRecord{
			"example.com": {
				{
					ID:        "1",
					AccountID: "1",
					Name:      "abc",
					Value:     "12.12.12.1",
					Type:      "A",
					TTL:       600,
				},
			},
		}

		updateByZone := map[string][]*endpoint.Endpoint{
			"example.com": {
				endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeA, "1.2.3.4"),
			},
		}
		var civoChanges CivoChanges

		err := processCreateActions(zonesByID, recordsByZoneID, updateByZone, &civoChanges)
		require.NoError(t, err)
		assert.Len(t, civoChanges.Creates, 1)
		assert.Empty(t, civoChanges.Deletes)
		assert.Empty(t, civoChanges.Updates)
	})

	t.Run("Records found which should not exist", func(t *testing.T) {
		zonesByID := map[string]civogo.DNSDomain{
			"example.com": {
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
		}

		recordsByZoneID := map[string][]civogo.DNSRecord{
			"example.com": {},
		}

		updateByZone := map[string][]*endpoint.Endpoint{
			"example.com": {},
		}
		var civoChanges CivoChanges

		err := processCreateActions(zonesByID, recordsByZoneID, updateByZone, &civoChanges)
		require.NoError(t, err)
		assert.Empty(t, civoChanges.Creates)
		assert.Empty(t, civoChanges.Creates)
		assert.Empty(t, civoChanges.Updates)
	})
}
func TestCivoProcessCreateActions(t *testing.T) {
	zoneByID := map[string]civogo.DNSDomain{
		"example.com": {
			ID:        "1",
			AccountID: "1",
			Name:      "example.com",
		},
	}

	recordsByZoneID := map[string][]civogo.DNSRecord{
		"example.com": {
			{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "12.12.12.1",
				Type:        "A",
				TTL:         600,
			},
		},
	}

	createsByZone := map[string][]*endpoint.Endpoint{
		"example.com": {
			endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
			endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
		},
	}

	var changes CivoChanges
	err := processCreateActions(zoneByID, recordsByZoneID, createsByZone, &changes)
	require.NoError(t, err)

	assert.Len(t, changes.Creates, 2)
	assert.Empty(t, changes.Updates)
	assert.Empty(t, changes.Deletes)

	expectedCreates := []*CivoChangeCreate{
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			Options: &civogo.DNSRecordConfig{
				Type:  "A",
				Name:  "foo",
				Value: "1.2.3.4",
			},
		},
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			Options: &civogo.DNSRecordConfig{
				Type:  "CNAME",
				Name:  "txt",
				Value: "foo.example.com",
			},
		},
	}

	if !elementsMatch(t, expectedCreates, changes.Creates) {
		assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates))
	}
}

func TestCivoProcessCreateActionsWithError(t *testing.T) {
	zoneByID := map[string]civogo.DNSDomain{
		"example.com": {
			ID:        "1",
			AccountID: "1",
			Name:      "example.com",
		},
	}

	recordsByZoneID := map[string][]civogo.DNSRecord{
		"example.com": {
			{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "12.12.12.1",
				Type:        "A",
				TTL:         600,
			},
		},
	}

	createsByZone := map[string][]*endpoint.Endpoint{
		"example.com": {
			endpoint.NewEndpoint("foo.example.com", "AAAA", "1.2.3.4"),
			endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
		},
	}

	var changes CivoChanges
	err := processCreateActions(zoneByID, recordsByZoneID, createsByZone, &changes)
	require.Error(t, err)
	assert.Equal(t, "invalid Record Type: AAAA", err.Error())
}

func TestCivoProcessUpdateActionsWithError(t *testing.T) {
	log.SetOutput(io.Discard)
	zoneByID := map[string]civogo.DNSDomain{
		"example.com": {
			ID:        "1",
			AccountID: "1",
			Name:      "example.com",
		},
	}

	recordsByZoneID := map[string][]civogo.DNSRecord{
		"example.com": {
			{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "12.12.12.1",
				Type:        "A",
				TTL:         600,
			},
		},
	}

	updatesByZone := map[string][]*endpoint.Endpoint{
		"example.com": {
			endpoint.NewEndpoint("foo.example.com", "AAAA", "1.2.3.4"),
			endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
		},
	}

	var changes CivoChanges
	err := processUpdateActions(zoneByID, recordsByZoneID, updatesByZone, &changes)
	require.Error(t, err)
}

func TestCivoProcessUpdateActions(t *testing.T) {
	zoneByID := map[string]civogo.DNSDomain{
		"example.com": {
			ID:        "1",
			AccountID: "1",
			Name:      "example.com",
		},
	}

	recordsByZoneID := map[string][]civogo.DNSRecord{
		"example.com": {
			{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "1.2.3.4",
				Type:        "A",
				TTL:         600,
			},
			{
				ID:          "2",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "foo",
				Value:       "foo.example.com",
				Type:        "CNAME",
				TTL:         600,
			},
			{
				ID:          "3",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "bar",
				Value:       "10.10.10.1",
				Type:        "A",
				TTL:         600,
			},
		},
	}

	updatesByZone := map[string][]*endpoint.Endpoint{
		"example.com": {
			endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "10.20.30.40"),
			endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeCNAME, "bar.example.com"),
		},
	}

	var changes CivoChanges
	err := processUpdateActions(zoneByID, recordsByZoneID, updatesByZone, &changes)
	require.NoError(t, err)

	assert.Len(t, changes.Creates, 2)
	assert.Empty(t, changes.Updates)
	assert.Len(t, changes.Deletes, 2)

	expectedUpdate := []*CivoChangeCreate{
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			Options: &civogo.DNSRecordConfig{
				Type:  "A",
				Name:  "txt",
				Value: "10.20.30.40",
			},
		},
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			Options: &civogo.DNSRecordConfig{
				Type:  "CNAME",
				Name:  "foo",
				Value: "bar.example.com",
			},
		},
	}

	if !elementsMatch(t, expectedUpdate, changes.Creates) {
		assert.Failf(t, "diff: %s", cmp.Diff(expectedUpdate, changes.Creates))
	}

	expectedDelete := []*CivoChangeDelete{
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			DomainRecord: civogo.DNSRecord{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "1.2.3.4",
				Type:        "A",
				Priority:    0,
				TTL:         600,
			},
		},
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			DomainRecord: civogo.DNSRecord{
				ID:          "2",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "foo",
				Value:       "foo.example.com",
				Type:        "CNAME",
				Priority:    0,
				TTL:         600,
			},
		},
	}

	if !elementsMatch(t, expectedDelete, changes.Deletes) {
		assert.Failf(t, "diff: %s", cmp.Diff(expectedDelete, changes.Deletes))
	}
}

func TestCivoProcessDeleteAction(t *testing.T) {
	zoneByID := map[string]civogo.DNSDomain{
		"example.com": {
			ID:        "1",
			AccountID: "1",
			Name:      "example.com",
		},
	}

	recordsByZoneID := map[string][]civogo.DNSRecord{
		"example.com": {
			{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "1.2.3.4",
				Type:        "A",
				TTL:         600,
			},
			{
				ID:          "2",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "foo",
				Value:       "5.6.7.8",
				Type:        "A",
				TTL:         600,
			},
			{
				ID:          "3",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "bar",
				Value:       "10.10.10.1",
				Type:        "A",
				TTL:         600,
			},
		},
	}

	deleteByDomain := map[string][]*endpoint.Endpoint{
		"example.com": {
			endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "1.2.3.4"),
			endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "5.6.7.8"),
		},
	}

	var changes CivoChanges
	err := processDeleteActions(zoneByID, recordsByZoneID, deleteByDomain, &changes)
	require.NoError(t, err)

	assert.Empty(t, changes.Creates)
	assert.Empty(t, changes.Updates)
	assert.Len(t, changes.Deletes, 2)

	expectedDelete := []*CivoChangeDelete{
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			DomainRecord: civogo.DNSRecord{
				ID:          "1",
				AccountID:   "1",
				DNSDomainID: "1",
				Name:        "txt",
				Value:       "1.2.3.4",
				Type:        "A",
				TTL:         600,
			},
		},
		{
			Domain: civogo.DNSDomain{
				ID:        "1",
				AccountID: "1",
				Name:      "example.com",
			},
			DomainRecord: civogo.DNSRecord{
				ID:          "2",
				AccountID:   "1",
				DNSDomainID: "1",
				Type:        "A",
				Name:        "foo",
				Value:       "5.6.7.8",
				TTL:         600,
			},
		},
	}

	if !elementsMatch(t, expectedDelete, changes.Deletes) {
		assert.Failf(t, "diff: %s", cmp.Diff(expectedDelete, changes.Deletes))
	}
}

func TestCivoApplyChanges(t *testing.T) {
	log.SetOutput(io.Discard)
	client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{
		{
			Method: "GET",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody:  "",
					URL:          "/v2/dns",
					ResponseBody: `[{"id": "12345", "account_id": "1", "name": "example.com"}]`,
				},
				{
					RequestBody:  "",
					URL:          "/v2/dns/12345/records",
					ResponseBody: `[]`,
				},
			},
		},
	})
	defer server.Close()

	changes := &plan.Changes{}
	provider := &CivoProvider{
		Client: *client,
	}
	changes.Create = []*endpoint.Endpoint{
		{DNSName: "new.ext-dns-test.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA},
		{DNSName: "new.ext-dns-test-with-ttl.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100},
	}
	changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target"}}}
	changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.de", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target-old"}}}
	changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100}}
	err := provider.ApplyChanges(t.Context(), changes)
	assert.NoError(t, err)
}

func TestCivoApplyChangesError(t *testing.T) {
	log.SetOutput(io.Discard)
	client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{
		{
			Method: "GET",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody:  "",
					URL:          "/v2/dns",
					ResponseBody: `[{"id": "12345", "account_id": "1", "name": "example.com"}]`,
				},
				{
					RequestBody:  "",
					URL:          "/v2/dns/12345/records",
					ResponseBody: `[]`,
				},
			},
		},
	})

	defer server.Close()

	provider := &CivoProvider{
		Client: *client,
	}

	cases := []struct {
		Name    string
		changes *plan.Changes
	}{
		{
			Name: "invalid record type from processCreateActions",
			changes: &plan.Changes{
				Create: []*endpoint.Endpoint{
					endpoint.NewEndpoint("bad.example.com", "AAAA", "1.2.3.4"),
				},
			},
		},
		{
			Name: "invalid record type from processUpdateActions",
			changes: &plan.Changes{
				UpdateOld: []*endpoint.Endpoint{
					endpoint.NewEndpoint("bad.example.com", "AAAA", "1.2.3.4"),
				},
				UpdateNew: []*endpoint.Endpoint{
					endpoint.NewEndpoint("bad.example.com", "AAAA", "5.6.7.8"),
				},
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.Name, func(t *testing.T) {
			err := provider.ApplyChanges(t.Context(), tt.changes)
			assert.Equal(t, "invalid Record Type: AAAA", string(err.Error()))
		})
	}
}

func TestCivoProviderFetchZones(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns": `[
			{"id": "12345", "account_id": "1", "name": "example.com"},
			{"id": "12346", "account_id": "1", "name": "example.net"}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client: *client,
	}

	expected, err := client.ListDNSDomains()
	if err != nil {
		t.Errorf("should not fail, %s", err)
	}
	zones, err := provider.fetchZones()
	if err != nil {
		t.Fatal(err)
	}
	assert.ElementsMatch(t, zones, expected)
}
func TestCivoProviderFetchZonesWithFilter(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns": `[
			{"id": "12345", "account_id": "1", "name": "example.com"},
			{"id": "12346", "account_id": "1", "name": "example.net"}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client:       *client,
		domainFilter: endpoint.NewDomainFilter([]string{".com"}),
	}

	expected := []civogo.DNSDomain{
		{ID: "12345", Name: "example.com", AccountID: "1"},
	}

	actual, err := provider.fetchZones()
	if err != nil {
		t.Fatal(err)
	}
	assert.ElementsMatch(t, expected, actual)
}

func TestCivoProviderFetchRecords(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns/12345/records": `[
			{"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600},
			{"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client: *client,
	}

	expected, err := client.ListDNSRecords("12345")
	assert.NoError(t, err)

	actual, err := provider.fetchRecords("12345")
	assert.NoError(t, err)

	assert.ElementsMatch(t, expected, actual)
}

func TestCivoProviderFetchRecordsWithError(t *testing.T) {
	log.SetOutput(io.Discard)
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns/12345/records": `[
			{"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600},
			{"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600}
			]`,
	})
	defer server.Close()
	provider := &CivoProvider{
		Client: *client,
	}

	_, err := provider.fetchRecords("235698")
	assert.Error(t, err)
}

func TestCivo_getStrippedRecordName(t *testing.T) {
	assert.Empty(t, getStrippedRecordName(civogo.DNSDomain{
		Name: "foo.com",
	}, endpoint.Endpoint{
		DNSName: "foo.com",
	}))

	assert.Equal(t, "api", getStrippedRecordName(civogo.DNSDomain{
		Name: "foo.com",
	}, endpoint.Endpoint{
		DNSName: "api.foo.com",
	}))
}

func TestCivo_convertRecordType(t *testing.T) {
	record, err := convertRecordType("A")
	recordA := civogo.DNSRecordType(civogo.DNSRecordTypeA)
	require.NoError(t, err)
	assert.Equal(t, recordA, record)

	record, err = convertRecordType("CNAME")
	recordCName := civogo.DNSRecordType(civogo.DNSRecordTypeCName)
	require.NoError(t, err)
	assert.Equal(t, recordCName, record)

	record, err = convertRecordType("TXT")
	recordTXT := civogo.DNSRecordType(civogo.DNSRecordTypeTXT)
	require.NoError(t, err)
	assert.Equal(t, recordTXT, record)

	record, err = convertRecordType("SRV")
	recordSRV := civogo.DNSRecordType(civogo.DNSRecordTypeSRV)
	require.NoError(t, err)
	assert.Equal(t, recordSRV, record)

	_, err = convertRecordType("INVALID")
	require.Error(t, err)

	assert.Equal(t, "invalid Record Type: INVALID", err.Error())
}

func TestCivoProviderGetRecordID(t *testing.T) {
	zone := civogo.DNSDomain{
		ID:   "12345",
		Name: "test.com",
	}

	record := []civogo.DNSRecord{{
		ID:          "1",
		Type:        "A",
		Name:        "www",
		Value:       "10.0.0.0",
		DNSDomainID: "12345",
		TTL:         600,
	}, {
		ID:          "2",
		Type:        "A",
		Name:        "api",
		Value:       "10.0.0.1",
		DNSDomainID: "12345",
		TTL:         600,
	}}

	endPoint := endpoint.Endpoint{DNSName: "www.test.com", Targets: endpoint.Targets{"10.0.0.0"}, RecordType: "A"}
	id := getRecordID(record, zone, endPoint)

	assert.Equal(t, id[0].ID, record[0].ID)
}

func TestCivo_submitChangesCreate(t *testing.T) {
	log.SetOutput(io.Discard)
	client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{
		{
			Method: "POST",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody: `{"type":"MX","name":"mail","value":"10.0.0.1","priority":10,"ttl":600}`,
					URL:         "/v2/dns/12345/records",
					ResponseBody: `{
						"id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
						"account_id": "1",
						"domain_id": "12345",
						"name": "mail",
						"value": "10.0.0.1",
						"type": "MX",
						"priority": 10,
						"ttl": 600
					}`,
				},
			},
		},
		{
			Method: "DELETE",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					URL:          "/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					ResponseBody: `{"result": "success"}`,
				},
				{
					URL:          "/v2/dns/12345/records/error-record-id",
					ResponseBody: `{"result": "error", "error": "failed to delete record"}`,
				},
			},
		},
		{
			Method: "PUT",
			Value: []civogo.ValueAdvanceClientForTesting{
				{
					RequestBody: `{"type":"MX","name":"mail","value":"10.0.0.2","priority":10,"ttl":600}`,
					URL:         "/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					ResponseBody: `{
						"id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
						"account_id": "1",
						"domain_id": "12345",
						"name": "mail",
						"value": "10.0.0.2",
						"type": "MX",
						"priority": 10,
						"ttl": 600
					}`,
				},
				{
					RequestBody:  `{"type":"MX","name":"mail","value":"10.0.0.3","priority":10,"ttl":600}`,
					URL:          "/v2/dns/12345/records/error-record-id",
					ResponseBody: `{"result": "error", "error": "failed to update record"}`,
				},
			},
		},
	})
	defer server.Close()

	provider := &CivoProvider{
		Client: *client,
		DryRun: true,
	}

	cases := []struct {
		name           string
		changes        *CivoChanges
		expectedResult error
	}{
		{
			name:           "changes slice is empty",
			changes:        &CivoChanges{},
			expectedResult: nil,
		},
		{
			name: "changes slice has changes and update changes",
			changes: &CivoChanges{
				Creates: []*CivoChangeCreate{
					{
						Domain: civogo.DNSDomain{
							ID:        "12345",
							AccountID: "1",
							Name:      "example.com",
						},
						Options: &civogo.DNSRecordConfig{
							Type:     "MX",
							Name:     "mail",
							Value:    "10.0.0.1",
							Priority: 10,
							TTL:      600,
						},
					},
				},

				Updates: []*CivoChangeUpdate{
					{
						Domain: civogo.DNSDomain{
							ID:        "12345",
							AccountID: "2",
							Name:      "example.org",
						},
					},
					{
						Domain: civogo.DNSDomain{
							ID:        "67890",
							AccountID: "3",
							Name:      "example.COM",
						},
					},
				},
			},
			expectedResult: nil,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			err := provider.submitChanges(*c.changes)
			assert.NoError(t, err)
		})
	}
}

func TestCivo_submitChangesDelete(t *testing.T) {
	client, server, _ := civogo.NewClientForTesting(map[string]string{
		"/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3": `{"result": "success"}`,
	})
	defer server.Close()

	provider := &CivoProvider{
		Client: *client,
		DryRun: false,
	}

	changes := CivoChanges{
		Deletes: []*CivoChangeDelete{
			{
				Domain: civogo.DNSDomain{ID: "12345", AccountID: "1", Name: "example.com"},
				DomainRecord: civogo.DNSRecord{
					ID:          "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					AccountID:   "1",
					DNSDomainID: "12345",
					Name:        "mail",
					Value:       "10.0.0.2",
					Type:        "MX",
					Priority:    10,
					TTL:         600,
				},
			},
		},
	}

	err := provider.submitChanges(changes)
	assert.NoError(t, err)
}

func TestCivoChangesEmpty(t *testing.T) {
	// Test empty CivoChanges
	c := &CivoChanges{}
	assert.True(t, c.Empty())

	// Test CivoChanges with Creates
	c = &CivoChanges{
		Creates: []*CivoChangeCreate{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				Options: &civogo.DNSRecordConfig{
					Type:     civogo.DNSRecordTypeA,
					Name:     "www",
					Value:    "192.1.1.1",
					Priority: 0,
					TTL:      600,
				},
			},
		},
	}
	assert.False(t, c.Empty())

	// Test CivoChanges with Updates
	c = &CivoChanges{
		Updates: []*CivoChangeUpdate{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				DomainRecord: civogo.DNSRecord{
					ID:          "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					AccountID:   "1",
					DNSDomainID: "12345",
					Name:        "mail",
					Value:       "192.168.1.2",
					Type:        "MX",
					Priority:    10,
					TTL:         600,
				},
				Options: civogo.DNSRecordConfig{
					Type:     "MX",
					Name:     "mail",
					Value:    "192.168.1.3",
					Priority: 10,
					TTL:      600,
				},
			},
		},
	}
	assert.False(t, c.Empty())

	// Test CivoChanges with Deletes
	c = &CivoChanges{
		Deletes: []*CivoChangeDelete{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				DomainRecord: civogo.DNSRecord{
					ID:          "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					AccountID:   "1",
					DNSDomainID: "12345",
					Name:        "mail",
					Value:       "192.168.1.3",
					Type:        "MX",
					Priority:    10,
					TTL:         600,
				},
			},
		},
	}
	assert.False(t, c.Empty())

	// Test CivoChanges with Creates, Updates, and Deletes
	c = &CivoChanges{
		Creates: []*CivoChangeCreate{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				Options: &civogo.DNSRecordConfig{
					Type:     civogo.DNSRecordTypeA,
					Name:     "www",
					Value:    "192.1.1.1",
					Priority: 0,
					TTL:      600,
				},
			},
		},
		Updates: []*CivoChangeUpdate{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				DomainRecord: civogo.DNSRecord{
					ID:          "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					AccountID:   "1",
					DNSDomainID: "12345",
					Name:        "mail",
					Value:       "192.168.1.2",
					Type:        "MX",
					Priority:    10,
					TTL:         600,
				},
				Options: civogo.DNSRecordConfig{
					Type:     "MX",
					Name:     "mail",
					Value:    "192.168.1.3",
					Priority: 10,
					TTL:      600,
				},
			},
		},
		Deletes: []*CivoChangeDelete{
			{
				Domain: civogo.DNSDomain{
					ID:        "12345",
					AccountID: "1",
					Name:      "example.com",
				},
				DomainRecord: civogo.DNSRecord{
					ID:          "76cc107f-fbef-4e2b-b97f-f5d34f4075d3",
					AccountID:   "1",
					DNSDomainID: "12345",
					Name:        "mail",
					Value:       "192.168.1.3",
					Type:        "MX",
					Priority:    10,
					TTL:         600,
				},
			},
		},
	}
	assert.False(t, c.Empty())
}

// This function is an adapted copy of the testify package's ElementsMatch function with the
// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to
// other structs. It also ignores ordering when comparing unlike cmp.Equal.
func elementsMatch(t *testing.T, listA, listB any) bool {
	switch {
	case listA == nil && listB == nil:
		return true
	case listA == nil:
		return isEmpty(listB)
	case listB == nil:
		return isEmpty(listA)
	}

	aKind := reflect.TypeOf(listA).Kind()
	bKind := reflect.TypeOf(listB).Kind()

	if aKind != reflect.Array && aKind != reflect.Slice {
		return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind))
	}

	if bKind != reflect.Array && bKind != reflect.Slice {
		return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind))
	}

	aValue := reflect.ValueOf(listA)
	bValue := reflect.ValueOf(listB)

	aLen := aValue.Len()
	bLen := bValue.Len()

	if aLen != bLen {
		return assert.Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen))
	}

	// Mark indexes in bValue that we already used
	visited := make([]bool, bLen)
	for i := range aLen {
		element := aValue.Index(i).Interface()
		found := false
		for j := range bLen {
			if visited[j] {
				continue
			}
			if cmp.Equal(bValue.Index(j).Interface(), element) {
				visited[j] = true
				found = true
				break
			}
		}
		if !found {
			return assert.Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue))
		}
	}

	return true
}

func isEmpty(xs any) bool {
	if xs != nil {
		objValue := reflect.ValueOf(xs)
		return objValue.Len() == 0
	}
	return true
}
