/*
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 scaleway

import (
	"io"
	"os"
	"reflect"
	"testing"

	domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
	"github.com/scaleway/scaleway-sdk-go/scw"
	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"
)

type mockScalewayDomain struct {
	*domain.API
}

func (m *mockScalewayDomain) ListDNSZones(_ *domain.ListDNSZonesRequest, _ ...scw.RequestOption) (*domain.ListDNSZonesResponse, error) {
	return &domain.ListDNSZonesResponse{
		DNSZones: []*domain.DNSZone{
			{
				Domain:    "example.com",
				Subdomain: "",
			},
			{
				Domain:    "example.com",
				Subdomain: "test",
			},
			{
				Domain:    "dummy.me",
				Subdomain: "",
			},
			{
				Domain:    "dummy.me",
				Subdomain: "test",
			},
		},
	}, nil
}

func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRequest, _ ...scw.RequestOption) (*domain.ListDNSZoneRecordsResponse, error) {
	records := []*domain.Record{}
	if req.DNSZone == "example.com" {
		records = []*domain.Record{
			{
				Data:     "1.1.1.1",
				Name:     "one",
				TTL:      300,
				Priority: 0,
				Type:     domain.RecordTypeA,
			},
			{
				Data:     "1.1.1.2",
				Name:     "two",
				TTL:      300,
				Priority: 0,
				Type:     domain.RecordTypeA,
			},
			{
				Data:     "1.1.1.3",
				Name:     "two",
				TTL:      300,
				Priority: 0,
				Type:     domain.RecordTypeA,
			},
		}
	} else if req.DNSZone == "test.example.com" {
		records = []*domain.Record{
			{
				Data:     "1.1.1.1",
				Name:     "",
				TTL:      300,
				Priority: 0,
				Type:     domain.RecordTypeA,
			},
			{
				Data:     "test.example.com.",
				Name:     "two",
				TTL:      600,
				Priority: 30,
				Type:     domain.RecordTypeCNAME,
			},
		}
	}
	return &domain.ListDNSZoneRecordsResponse{
		Records: records,
	}, nil
}

func (m *mockScalewayDomain) UpdateDNSZoneRecords(_ *domain.UpdateDNSZoneRecordsRequest, _ ...scw.RequestOption) (*domain.UpdateDNSZoneRecordsResponse, error) {
	return &domain.UpdateDNSZoneRecordsResponse{}, nil
}

func TestScalewayProvider_NewScalewayProvider(t *testing.T) {
	profile := `profiles:
  foo:
    access_key: SCWXXXXXXXXXXXXXXXXX
    secret_key: 11111111-1111-1111-1111-111111111111
`
	tmpDir := t.TempDir()
	err := os.WriteFile(tmpDir+"/config.yaml", []byte(profile), 0600)
	if err != nil {
		t.Errorf("failed : %s", err)
	}
	t.Setenv(scw.ScwActiveProfileEnv, "foo")
	t.Setenv(scw.ScwConfigPathEnv, tmpDir+"/config.yaml")
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err != nil {
		t.Errorf("failed : %s", err)
	}

	t.Setenv(scw.ScwAccessKeyEnv, "SCWXXXXXXXXXXXXXXXXX")
	t.Setenv(scw.ScwSecretKeyEnv, "11111111-1111-1111-1111-111111111111")
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err != nil {
		t.Errorf("failed : %s", err)
	}

	_ = os.Unsetenv(scw.ScwSecretKeyEnv)
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err == nil {
		t.Errorf("expected to fail")
	}

	t.Setenv(scw.ScwSecretKeyEnv, "dummy")
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err == nil {
		t.Errorf("expected to fail")
	}

	_ = os.Unsetenv(scw.ScwAccessKeyEnv)
	t.Setenv(scw.ScwSecretKeyEnv, "11111111-1111-1111-1111-111111111111")
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err == nil {
		t.Errorf("expected to fail")
	}

	t.Setenv(scw.ScwAccessKeyEnv, "dummy")
	_, err = newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	if err == nil {
		t.Errorf("expected to fail")
	}
}

func TestScalewayProvider_OptionnalConfigFile(t *testing.T) {
	log.SetOutput(io.Discard)
	t.Setenv(scw.ScwAccessKeyEnv, "SCWXXXXXXXXXXXXXXXXX")
	t.Setenv(scw.ScwSecretKeyEnv, "11111111-1111-1111-1111-111111111111")

	_, err := newProvider(endpoint.NewDomainFilter([]string{"example.com"}), true)
	assert.NoError(t, err)
}

func TestScalewayProvider_AdjustEndpoints(t *testing.T) {
	provider := &ScalewayProvider{}

	before := []*endpoint.Endpoint{
		{
			DNSName:    "one.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
		{
			DNSName:    "two.example.com",
			RecordTTL:  0,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "10",
				},
			},
		},
		{
			DNSName:          "three.example.com",
			RecordTTL:        600,
			RecordType:       "A",
			Targets:          []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{},
		},
	}

	expected := []*endpoint.Endpoint{
		{
			DNSName:    "one.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
		{
			DNSName:    "two.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "10",
				},
			},
		},
		{
			DNSName:    "three.example.com",
			RecordTTL:  600,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
	}

	after, err := provider.AdjustEndpoints(before)
	require.NoError(t, err)
	for i := range after {
		if !checkRecordEquality(after[i], expected[i]) {
			t.Errorf("got record %s instead of %s", after[i], expected[i])
		}
	}
}

func TestScalewayProvider_Zones(t *testing.T) {
	mocked := mockScalewayDomain{nil}
	provider := &ScalewayProvider{
		domainAPI:    &mocked,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	expected := []*domain.DNSZone{
		{
			Domain:    "example.com",
			Subdomain: "",
		},
		{
			Domain:    "example.com",
			Subdomain: "test",
		},
	}
	zones, err := provider.Zones(t.Context())
	if err != nil {
		t.Fatal(err)
	}
	require.Len(t, zones, len(expected))
	for i, zone := range zones {
		assert.Equal(t, expected[i], zone)
	}
}

func TestScalewayProvider_Records(t *testing.T) {
	mocked := mockScalewayDomain{nil}
	provider := &ScalewayProvider{
		domainAPI:    &mocked,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	expected := []*endpoint.Endpoint{
		{
			DNSName:    "one.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
		{
			DNSName:    "two.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.2", "1.1.1.3"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
		{
			DNSName:    "test.example.com",
			RecordTTL:  300,
			RecordType: "A",
			Targets:    []string{"1.1.1.1"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "0",
				},
			},
		},
		{
			DNSName:    "two.test.example.com",
			RecordTTL:  600,
			RecordType: "CNAME",
			Targets:    []string{"test.example.com"},
			ProviderSpecific: endpoint.ProviderSpecific{
				{
					Name:  scalewayPriorityKey,
					Value: "30",
				},
			},
		},
	}

	records, err := provider.Records(t.Context())
	if err != nil {
		t.Fatal(err)
	}

	require.Len(t, records, len(expected))
	for _, record := range records {
		found := false
		for _, expectedRecord := range expected {
			if checkRecordEquality(record, expectedRecord) {
				found = true
			}
		}
		assert.True(t, found)
	}
}

// this test is really ugly since we are working on maps, so array are randomly sorted
// feel free to modify if you have a better idea
func TestScalewayProvider_generateApplyRequests(t *testing.T) {
	mocked := mockScalewayDomain{nil}
	provider := &ScalewayProvider{
		domainAPI:    &mocked,
		domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
	}

	expected := []*domain.UpdateDNSZoneRecordsRequest{
		{
			DNSZone: "example.com",
			Changes: []*domain.RecordChange{
				{
					Add: &domain.RecordChangeAdd{
						Records: []*domain.Record{
							{
								Data:     "1.1.1.1",
								Name:     "",
								TTL:      300,
								Type:     domain.RecordTypeA,
								Priority: 0,
							},
							{
								Data:     "1.1.1.2",
								Name:     "",
								TTL:      300,
								Type:     domain.RecordTypeA,
								Priority: 0,
							},
							{
								Data:     "2.2.2.2",
								Name:     "me",
								TTL:      600,
								Type:     domain.RecordTypeA,
								Priority: 30,
							},
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("3.3.3.3"),
							Name: "me",
							Type: domain.RecordTypeA,
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("1.1.1.1"),
							Name: "here",
							Type: domain.RecordTypeA,
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("1.1.1.2"),
							Name: "here",
							Type: domain.RecordTypeA,
						},
					},
				},
			},
		},
		{
			DNSZone: "test.example.com",
			Changes: []*domain.RecordChange{
				{
					Add: &domain.RecordChangeAdd{
						Records: []*domain.Record{
							{
								Data:     "example.com.",
								Name:     "",
								TTL:      600,
								Type:     domain.RecordTypeCNAME,
								Priority: 20,
							},
							{
								Data:     "1.2.3.4",
								Name:     "my",
								TTL:      300,
								Type:     domain.RecordTypeA,
								Priority: 0,
							},
							{
								Data:     "5.6.7.8",
								Name:     "my",
								TTL:      300,
								Type:     domain.RecordTypeA,
								Priority: 0,
							},
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("1.1.1.1"),
							Name: "here.is.my",
							Type: domain.RecordTypeA,
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("4.4.4.4"),
							Name: "my",
							Type: domain.RecordTypeA,
						},
					},
				},
				{
					Delete: &domain.RecordChangeDelete{
						IDFields: &domain.RecordIdentifier{
							Data: new("5.5.5.5"),
							Name: "my",
							Type: domain.RecordTypeA,
						},
					},
				},
			},
		},
	}

	changes := &plan.Changes{
		Create: []*endpoint.Endpoint{
			{
				DNSName:    "example.com",
				RecordType: "A",
				Targets:    []string{"1.1.1.1", "1.1.1.2"},
			},
			{
				DNSName:    "test.example.com",
				RecordType: "CNAME",
				ProviderSpecific: endpoint.ProviderSpecific{
					{
						Name:  scalewayPriorityKey,
						Value: "20",
					},
				},
				RecordTTL: 600,
				Targets:   []string{"example.com"},
			},
		},
		Delete: []*endpoint.Endpoint{
			{
				DNSName:    "here.example.com",
				RecordType: "A",
				Targets:    []string{"1.1.1.1", "1.1.1.2"},
			},
			{
				DNSName:    "here.is.my.test.example.com",
				RecordType: "A",
				Targets:    []string{"1.1.1.1"},
			},
		},
		UpdateNew: []*endpoint.Endpoint{
			{
				DNSName: "me.example.com",
				ProviderSpecific: endpoint.ProviderSpecific{
					{
						Name:  scalewayPriorityKey,
						Value: "30",
					},
				},
				RecordType: "A",
				RecordTTL:  600,
				Targets:    []string{"2.2.2.2"},
			},
			{
				DNSName:    "my.test.example.com",
				RecordType: "A",
				Targets:    []string{"1.2.3.4", "5.6.7.8"},
			},
		},
		UpdateOld: []*endpoint.Endpoint{
			{
				DNSName: "me.example.com",
				ProviderSpecific: endpoint.ProviderSpecific{
					{
						Name:  scalewayPriorityKey,
						Value: "1234",
					},
				},
				RecordType: "A",
				Targets:    []string{"3.3.3.3"},
			},
			{
				DNSName:    "my.test.example.com",
				RecordType: "A",
				Targets:    []string{"4.4.4.4", "5.5.5.5"},
			},
		},
	}

	requests, err := provider.generateApplyRequests(t.Context(), changes)
	if err != nil {
		t.Fatal(err)
	}

	require.Len(t, requests, len(expected))
	total := int(len(expected))
	for _, req := range requests {
		for _, exp := range expected {
			if checkScalewayReqChanges(req, exp) {
				total--
			}
		}
	}
	assert.Equal(t, 0, total)
}

func checkRecordEquality(record1, record2 *endpoint.Endpoint) bool {
	return record1.Targets.Same(record2.Targets) &&
		record1.DNSName == record2.DNSName &&
		record1.RecordTTL == record2.RecordTTL &&
		record1.RecordType == record2.RecordType &&
		reflect.DeepEqual(record1.ProviderSpecific, record2.ProviderSpecific)
}

func checkScalewayReqChanges(r1, r2 *domain.UpdateDNSZoneRecordsRequest) bool {
	if r1.DNSZone != r2.DNSZone {
		return false
	}
	if len(r1.Changes) != len(r2.Changes) {
		return false
	}
	total := int(len(r1.Changes))
	for _, c1 := range r1.Changes {
		for _, c2 := range r2.Changes {
			// we only have 1 add per request
			if c1.Add != nil && c2.Add != nil && checkScalewayRecords(c1.Add.Records, c2.Add.Records) {
				total--
			} else if c1.Delete != nil && c2.Delete != nil {
				if *c1.Delete.IDFields.Data == *c2.Delete.IDFields.Data && c1.Delete.IDFields.Name == c2.Delete.IDFields.Name && c1.Delete.IDFields.Type == c2.Delete.IDFields.Type {
					total--
				}
			}
		}
	}
	return total == 0
}

func checkScalewayRecords(rs1, rs2 []*domain.Record) bool {
	if len(rs1) != len(rs2) {
		return false
	}
	total := int(len(rs1))
	for _, r1 := range rs1 {
		for _, r2 := range rs2 {
			if r1.Data == r2.Data &&
				r1.Name == r2.Name &&
				r1.Priority == r2.Priority &&
				r1.TTL == r2.TTL &&
				r1.Type == r2.Type {
				total--
			}
		}
	}
	return total == 0
}
