/*
Copyright 2017 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 plan

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"

	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"sigs.k8s.io/external-dns/endpoint"
	"sigs.k8s.io/external-dns/internal/testutils"
	logtest "sigs.k8s.io/external-dns/internal/testutils/log"
)

type PlanTestSuite struct {
	suite.Suite
	fooV1Cname                       *endpoint.Endpoint
	fooV2Cname                       *endpoint.Endpoint
	fooV2CnameUppercase              *endpoint.Endpoint
	fooV2TXT                         *endpoint.Endpoint
	fooV2CnameNoLabel                *endpoint.Endpoint
	fooV3CnameSameResource           *endpoint.Endpoint
	fooA5                            *endpoint.Endpoint
	fooAAAA                          *endpoint.Endpoint
	dsA                              *endpoint.Endpoint
	dsAAAA                           *endpoint.Endpoint
	bar127A                          *endpoint.Endpoint
	bar127AWithTTL                   *endpoint.Endpoint
	bar127AWithProviderSpecificTrue  *endpoint.Endpoint
	bar127AWithProviderSpecificFalse *endpoint.Endpoint
	bar127AWithProviderSpecificUnset *endpoint.Endpoint
	bar192A                          *endpoint.Endpoint
	multiple1                        *endpoint.Endpoint
	multiple2                        *endpoint.Endpoint
	multiple3                        *endpoint.Endpoint
	domainFilterFiltered1            *endpoint.Endpoint
	domainFilterFiltered2            *endpoint.Endpoint
	domainFilterFiltered3            *endpoint.Endpoint
	domainFilterExcluded             *endpoint.Endpoint
}

func (suite *PlanTestSuite) SetupTest() {
	suite.fooV1Cname = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"v1"},
		RecordType: "CNAME",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-v1",
			endpoint.OwnerLabelKey:    "pwner",
		},
	}
	// same resource as fooV1Cname, but target is different. It will never be picked because its target lexicographically bigger than "v1"
	suite.fooV3CnameSameResource = &endpoint.Endpoint{ // TODO: remove this once endpoint can support multiple targets
		DNSName:    "foo",
		Targets:    endpoint.Targets{"v3"},
		RecordType: "CNAME",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-v1",
			endpoint.OwnerLabelKey:    "pwner",
		},
	}
	suite.fooV2Cname = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"v2"},
		RecordType: "CNAME",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-v2",
		},
	}
	suite.fooV2CnameUppercase = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"V2"},
		RecordType: "CNAME",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-v2",
		},
	}
	suite.fooV2TXT = &endpoint.Endpoint{
		DNSName:    "foo",
		RecordType: "TXT",
	}
	suite.fooV2CnameNoLabel = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"v2"},
		RecordType: "CNAME",
	}
	suite.fooA5 = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"5.5.5.5"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-5",
		},
	}
	suite.fooAAAA = &endpoint.Endpoint{
		DNSName:    "foo",
		Targets:    endpoint.Targets{"2001:DB8::1"},
		RecordType: "AAAA",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/foo-AAAA",
		},
	}
	suite.dsA = &endpoint.Endpoint{
		DNSName:    "ds",
		Targets:    endpoint.Targets{"1.1.1.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/ds",
		},
	}
	suite.dsAAAA = &endpoint.Endpoint{
		DNSName:    "ds",
		Targets:    endpoint.Targets{"2001:DB8::1"},
		RecordType: "AAAA",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/ds-AAAAA",
		},
	}
	suite.bar127A = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"127.0.0.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-127",
		},
	}
	suite.bar127AWithTTL = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"127.0.0.1"},
		RecordType: "A",
		RecordTTL:  300,
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-127",
		},
	}
	suite.bar127AWithProviderSpecificTrue = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"127.0.0.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-127",
		},
		ProviderSpecific: endpoint.ProviderSpecific{
			endpoint.ProviderSpecificProperty{
				Name:  "alias",
				Value: "false",
			},
			endpoint.ProviderSpecificProperty{
				Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
				Value: "true",
			},
		},
	}
	suite.bar127AWithProviderSpecificFalse = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"127.0.0.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-127",
		},
		ProviderSpecific: endpoint.ProviderSpecific{
			endpoint.ProviderSpecificProperty{
				Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
				Value: "false",
			},
			endpoint.ProviderSpecificProperty{
				Name:  "alias",
				Value: "false",
			},
		},
	}
	suite.bar127AWithProviderSpecificUnset = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"127.0.0.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-127",
		},
		ProviderSpecific: endpoint.ProviderSpecific{
			endpoint.ProviderSpecificProperty{
				Name:  "alias",
				Value: "false",
			},
		},
	}
	suite.bar192A = &endpoint.Endpoint{
		DNSName:    "bar",
		Targets:    endpoint.Targets{"192.168.0.1"},
		RecordType: "A",
		Labels: map[string]string{
			endpoint.ResourceLabelKey: "ingress/default/bar-192",
		},
	}
	suite.multiple1 = &endpoint.Endpoint{
		DNSName:       "multiple",
		Targets:       endpoint.Targets{"192.168.0.1"},
		RecordType:    "A",
		SetIdentifier: "test-set-1",
	}
	suite.multiple2 = &endpoint.Endpoint{
		DNSName:       "multiple",
		Targets:       endpoint.Targets{"192.168.0.2"},
		RecordType:    "A",
		SetIdentifier: "test-set-1",
	}
	suite.multiple3 = &endpoint.Endpoint{
		DNSName:       "multiple",
		Targets:       endpoint.Targets{"192.168.0.2"},
		RecordType:    "A",
		SetIdentifier: "test-set-2",
	}
	suite.domainFilterFiltered1 = &endpoint.Endpoint{
		DNSName:    "foo.domain.tld",
		Targets:    endpoint.Targets{"1.2.3.4"},
		RecordType: "A",
	}
	suite.domainFilterFiltered2 = &endpoint.Endpoint{
		DNSName:    "bar.domain.tld",
		Targets:    endpoint.Targets{"1.2.3.5"},
		RecordType: "A",
	}
	suite.domainFilterFiltered3 = &endpoint.Endpoint{
		DNSName:    "baz.domain.tld",
		Targets:    endpoint.Targets{"1.2.3.6"},
		RecordType: "A",
	}
	suite.domainFilterExcluded = &endpoint.Endpoint{
		DNSName:    "foo.ex.domain.tld",
		Targets:    endpoint.Targets{"1.1.1.1"},
		RecordType: "A",
	}
}

func TestPlan_ChangesJson_DecodeEncode(t *testing.T) {
	ch := &Changes{
		Create: []*endpoint.Endpoint{
			{
				DNSName: "foo",
			},
		},
		UpdateOld: []*endpoint.Endpoint{
			{
				DNSName: "bar",
			},
		},
		UpdateNew: []*endpoint.Endpoint{
			{
				DNSName: "baz",
			},
		},
		Delete: []*endpoint.Endpoint{
			{
				DNSName: "qux",
			},
		},
	}
	jsonBytes, err := json.Marshal(ch)
	require.NoError(t, err)
	assert.JSONEq(t,
		`{"create":[{"dnsName":"foo"}],"updateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"delete":[{"dnsName":"qux"}]}`,
		string(jsonBytes))
	var changes Changes
	err = json.NewDecoder(bytes.NewBuffer(jsonBytes)).Decode(&changes)
	require.NoError(t, err)
	assert.Equal(t, ch, &changes)
}

func TestPlan_ChangesJson_DecodeMixedCase(t *testing.T) {
	input := `{"Create":[{"dnsName":"foo"}],"UpdateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"Delete":[{"dnsName":"qux"}]}`
	var changes Changes
	err := json.NewDecoder(strings.NewReader(input)).Decode(&changes)
	require.NoError(t, err)
	assert.Len(t, changes.Create, 1)
}

func (suite *PlanTestSuite) TestSyncFirstRound() {
	current := []*endpoint.Endpoint{}
	desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname, suite.bar127A}
	expectedCreate := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar127A} // v1 is chosen because of resolver taking "min"
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRound() {
	current := []*endpoint.Endpoint{suite.fooV1Cname}
	desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A}
	expectedCreate := []*endpoint.Endpoint{suite.bar127A}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
	current := []*endpoint.Endpoint{suite.fooV2CnameNoLabel}
	desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A}
	expectedCreate := []*endpoint.Endpoint{suite.bar127A}
	expectedUpdateOld := []*endpoint.Endpoint{suite.fooV2CnameNoLabel}
	expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
	current := []*endpoint.Endpoint{suite.bar127A}
	desired := []*endpoint.Endpoint{suite.bar127AWithTTL}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.bar127A}
	expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithTTL}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
	current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificNoChange() {
	current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	suite.False(changes.HasChanges())
}

func (suite *PlanTestSuite) TestHasChangesCreate() {
	changes := &Changes{
		Create: []*endpoint.Endpoint{suite.fooV1Cname},
	}
	suite.True(changes.HasChanges())
}

func (suite *PlanTestSuite) TestHasChangesDelete() {
	changes := &Changes{
		Delete: []*endpoint.Endpoint{suite.fooV1Cname},
	}
	suite.True(changes.HasChanges())
}

func (suite *PlanTestSuite) TestHasChanges() {
	current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	suite.True(changes.HasChanges())
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificRemoval() {
	current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
	desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
	expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificAddition() {
	current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
	desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
	expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
	current := []*endpoint.Endpoint{suite.fooV1Cname}
	desired := []*endpoint.Endpoint{suite.fooV2Cname}

	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname}
	expectedUpdateNew := []*endpoint.Endpoint{{
		DNSName:    suite.fooV2Cname.DNSName,
		Targets:    suite.fooV2Cname.Targets,
		RecordType: suite.fooV2Cname.RecordType,
		RecordTTL:  suite.fooV2Cname.RecordTTL,
		Labels: map[string]string{
			endpoint.ResourceLabelKey: suite.fooV2Cname.Labels[endpoint.ResourceLabelKey],
			endpoint.OwnerLabelKey:    suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
		},
	}}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestIdempotency() {
	current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname}
	desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies: []Policy{&SyncPolicy{}},
		Current:  current,
		Desired:  desired,
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestRecordTypeChange() {
	current := []*endpoint.Endpoint{suite.fooV1Cname}
	desired := []*endpoint.Endpoint{suite.fooA5}
	expectedCreate := []*endpoint.Endpoint{suite.fooA5}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestExistingCNameWithDualStackDesired() {
	current := []*endpoint.Endpoint{suite.fooV1Cname}
	desired := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
	expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestExistingDualStackWithCNameDesired() {
	suite.fooA5.Labels[endpoint.OwnerLabelKey] = "nerf"
	suite.fooAAAA.Labels[endpoint.OwnerLabelKey] = "nerf"
	current := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
	desired := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooA5.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

// TestExistingOwnerNotMatchingDualStackDesired validates that if there is an existing
// record for a domain but there is no ownership claim over it and there are desired
// records no changes are planed. Only domains that have explicit ownership claims should
// be updated.
func (suite *PlanTestSuite) TestExistingOwnerNotMatchingDualStackDesired() {
	suite.fooA5.Labels = nil
	current := []*endpoint.Endpoint{suite.fooA5}
	desired := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        "pwner",
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

// TestConflictingCurrentNonConflictingDesired is a bit of a corner case as it would indicate
// that the provider is not following valid DNS rules or there may be some
// caching issues. In this case since the desired records are not conflicting
// the updates will end up with the conflict resolved.
func (suite *PlanTestSuite) TestConflictingCurrentNonConflictingDesired() {
	suite.fooA5.Labels[endpoint.OwnerLabelKey] = suite.fooV1Cname.Labels[endpoint.OwnerLabelKey]
	current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}
	desired := []*endpoint.Endpoint{suite.fooA5}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

// TestConflictingCurrentNoDesired is a bit of a corner case as it would indicate
// that the provider is not following valid DNS rules or there may be some
// caching issues. In this case there are no desired enpoint candidates so plan
// on deleting the records.
func (suite *PlanTestSuite) TestConflictingCurrentNoDesired() {
	suite.fooA5.Labels[endpoint.OwnerLabelKey] = suite.fooV1Cname.Labels[endpoint.OwnerLabelKey]
	current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}
	desired := []*endpoint.Endpoint{}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

// TestCurrentWithConflictingDesired simulates where the desired records result in conflicting records types.
// This could be the result of multiple sources generating conflicting records types. In this case the conflict
// resolver should prefer the A and AAAA record candidate and delete the other records.
func (suite *PlanTestSuite) TestCurrentWithConflictingDesired() {
	suite.fooV1Cname.Labels[endpoint.OwnerLabelKey] = "nerf"
	current := []*endpoint.Endpoint{suite.fooV1Cname}
	desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5, suite.fooAAAA}
	expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

// TestNoCurrentWithConflictingDesired simulates where the desired records result in conflicting records types.
// This could be the result of multiple sources generating conflicting records types. In this case, the
// conflict resolver should prefer the A and AAAA record and drop the other candidate record types.
func (suite *PlanTestSuite) TestNoCurrentWithConflictingDesired() {
	current := []*endpoint.Endpoint{}
	desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5, suite.fooAAAA}
	expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestIgnoreTXT() {
	current := []*endpoint.Endpoint{suite.fooV2TXT}
	desired := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestExcludeTXT() {
	current := []*endpoint.Endpoint{suite.fooV2TXT}
	desired := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeTXT},
		ExcludeRecords: []string{endpoint.RecordTypeTXT},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestIgnoreTargetCase() {
	current := []*endpoint.Endpoint{suite.fooV2Cname}
	desired := []*endpoint.Endpoint{suite.fooV2CnameUppercase}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies: []Policy{&SyncPolicy{}},
		Current:  current,
		Desired:  desired,
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestRemoveEndpoint() {
	current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
	desired := []*endpoint.Endpoint{suite.fooV1Cname}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.bar192A}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
	current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
	desired := []*endpoint.Endpoint{suite.fooV1Cname}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&UpsertOnlyPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() {
	current := []*endpoint.Endpoint{suite.multiple1}
	desired := []*endpoint.Endpoint{suite.multiple2, suite.multiple3}
	expectedCreate := []*endpoint.Endpoint{suite.multiple3}
	expectedUpdateOld := []*endpoint.Endpoint{suite.multiple1}
	expectedUpdateNew := []*endpoint.Endpoint{suite.multiple2}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
	current := []*endpoint.Endpoint{suite.multiple2}
	desired := []*endpoint.Endpoint{suite.multiple3}
	expectedCreate := []*endpoint.Endpoint{suite.multiple3}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.multiple2}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestDomainFiltersInitial() {
	current := []*endpoint.Endpoint{suite.domainFilterExcluded}
	desired := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3}
	expectedCreate := []*endpoint.Endpoint{suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	domainFilter := endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"})
	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		DomainFilter:   endpoint.MatchAllDomainFilters{domainFilter},
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
	current := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2}
	desired := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3}
	expectedCreate := []*endpoint.Endpoint{suite.domainFilterFiltered3}
	expectedUpdateOld := []*endpoint.Endpoint{}
	expectedUpdateNew := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{}

	domainFilter := endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"})
	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		DomainFilter:   endpoint.MatchAllDomainFilters{domainFilter},
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestAAAARecords() {
	current := []*endpoint.Endpoint{}
	desired := []*endpoint.Endpoint{suite.fooAAAA}
	expectedCreate := []*endpoint.Endpoint{suite.fooAAAA}
	expectNoChanges := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.Delete, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateOld, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateNew, expectNoChanges)
}

func (suite *PlanTestSuite) TestDualStackRecords() {
	current := []*endpoint.Endpoint{}
	desired := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
	expectedCreate := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
	expectNoChanges := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.Delete, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateOld, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateNew, expectNoChanges)
}

func (suite *PlanTestSuite) TestDualStackRecordsDelete() {
	current := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
	desired := []*endpoint.Endpoint{}
	expectedDelete := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
	expectNoChanges := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Delete, expectedDelete)
	validateEntries(suite.T(), changes.Create, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateOld, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateNew, expectNoChanges)
}

func (suite *PlanTestSuite) TestDualStackToSingleStack() {
	current := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
	desired := []*endpoint.Endpoint{suite.dsA}
	expectedDelete := []*endpoint.Endpoint{suite.dsAAAA}
	expectNoChanges := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
	}

	changes := p.Calculate().Changes
	validateEntries(suite.T(), changes.Delete, expectedDelete)
	validateEntries(suite.T(), changes.Create, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateOld, expectNoChanges)
	validateEntries(suite.T(), changes.UpdateNew, expectNoChanges)
}

func (suite *PlanTestSuite) TestRecordOwnerIdMigration() {
	suite.fooA5.Labels[endpoint.OwnerLabelKey] = "bar"
	current := []*endpoint.Endpoint{suite.fooA5}
	desired := []*endpoint.Endpoint{suite.fooA5}
	expectedCreate := []*endpoint.Endpoint{}
	expectedUpdateOld := []*endpoint.Endpoint{suite.fooA5}
	expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5}
	expectedDelete := []*endpoint.Endpoint{}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        current,
		Desired:        desired,
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
		OwnerID:        suite.fooA5.Labels[endpoint.OwnerLabelKey],
		OldOwnerID:     "foo",
	}

	changes := p.Calculate().Changes

	validateEntries(suite.T(), changes.Create, expectedCreate)
	validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
	validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
	validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func TestPlan(t *testing.T) {
	suite.Run(t, new(PlanTestSuite))
}

// validateEntries validates that the list of entries matches expected.
func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
	if !testutils.SameEndpoints(entries, expected) {
		t.Fatalf("expected %q to match %q", entries, expected)
	}
}

func TestShouldUpdateProviderSpecific(tt *testing.T) {
	for _, test := range []struct {
		name         string
		current      *endpoint.Endpoint
		desired      *endpoint.Endpoint
		shouldUpdate bool
	}{
		{
			name: "skip AWS target health",
			current: &endpoint.Endpoint{
				DNSName: "foo.com",
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "aws/evaluate-target-health", Value: "true"},
				},
			},
			desired: &endpoint.Endpoint{
				DNSName: "bar.com",
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "aws/evaluate-target-health", Value: "true"},
				},
			},
			shouldUpdate: false,
		},
		{
			name: "custom property unchanged",
			current: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "custom/property", Value: "true"},
				},
			},
			desired: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "custom/property", Value: "true"},
				},
			},
			shouldUpdate: false,
		},
		{
			name: "custom property value changed",
			current: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "custom/property", Value: "true"},
				},
			},
			desired: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "custom/property", Value: "false"},
				},
			},
			shouldUpdate: true,
		},
		{
			name: "custom property key changed",
			current: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "custom/property", Value: "true"},
				},
			},
			desired: &endpoint.Endpoint{
				ProviderSpecific: []endpoint.ProviderSpecificProperty{
					{Name: "new/property", Value: "true"},
				},
			},
			shouldUpdate: true,
		},
	} {
		tt.Run(test.name, func(t *testing.T) {
			plan := &Plan{
				Current:        []*endpoint.Endpoint{test.current},
				Desired:        []*endpoint.Endpoint{test.desired},
				ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
			}
			b := plan.providerSpecificChanged(test.desired, test.current)
			assert.Equal(t, test.shouldUpdate, b)
		})
	}
}

func TestOwnerMismatchLogsDebug(t *testing.T) {
	const wantMsg = "owner id does not match"

	// current A record owned by someone else; desired CNAME owned by us.
	// The CNAME has no current record → triggers a create, which activates
	// the owner-check block and the debug log.
	current := &endpoint.Endpoint{
		DNSName:    "foo",
		RecordType: endpoint.RecordTypeA,
		Targets:    endpoint.Targets{"1.2.3.4"},
		Labels:     map[string]string{endpoint.OwnerLabelKey: "other"},
	}
	desired := &endpoint.Endpoint{
		DNSName:    "foo",
		RecordType: endpoint.RecordTypeCNAME,
		Targets:    endpoint.Targets{"bar.example.com"},
		Labels:     map[string]string{endpoint.OwnerLabelKey: "pwner"},
	}

	p := &Plan{
		Policies:       []Policy{&SyncPolicy{}},
		Current:        []*endpoint.Endpoint{current},
		Desired:        []*endpoint.Endpoint{desired},
		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
		OwnerID:        "pwner",
	}

	hook := logtest.LogsUnderTestWithLogLevel(log.DebugLevel, t)
	p.Calculate()
	logtest.TestHelperLogContainsWithLogLevel(wantMsg, log.DebugLevel, hook, t)
}
