/*
Copyright 2019 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 dns

import (
	"bytes"
	"context"
	"errors"
	"io"
	"net/http"
	"testing"

	"github.com/digitalocean/godo"

	"k8s.io/kops/dnsprovider/pkg/dnsprovider/rrstype"
)

type fakeDomainService struct {
	listFunc                 func(ctx context.Context, listOpt *godo.ListOptions) ([]godo.Domain, *godo.Response, error)
	getFunc                  func(ctx context.Context, name string) (*godo.Domain, *godo.Response, error)
	createFunc               func(ctx context.Context, domainCreateRequest *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error)
	deleteFunc               func(ctx context.Context, name string) (*godo.Response, error)
	recordsFunc              func(ctx context.Context, domain string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error)
	recordsByNameFunc        func(ctx context.Context, domain string, name string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error)
	recordsByTypeFunc        func(ctx context.Context, domain string, name string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error)
	recordsByTypeAndNameFunc func(ctx context.Context, domain string, ofType string, name string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error)

	recordFunc       func(ctx context.Context, domain string, id int) (*godo.DomainRecord, *godo.Response, error)
	deleteRecordFunc func(ctx context.Context, domain string, id int) (*godo.Response, error)
	editRecordFunc   func(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error)
	createRecordFunc func(ctx context.Context, domain string, createRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error)
}

func (f *fakeDomainService) List(ctx context.Context, listOpt *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
	return f.listFunc(ctx, listOpt)
}

func (f *fakeDomainService) Get(ctx context.Context, name string) (*godo.Domain, *godo.Response, error) {
	return f.getFunc(ctx, name)
}

func (f *fakeDomainService) Create(ctx context.Context, domainCreateRequest *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) {
	return f.createFunc(ctx, domainCreateRequest)
}

func (f *fakeDomainService) Delete(ctx context.Context, name string) (*godo.Response, error) {
	return f.deleteFunc(ctx, name)
}

func (f *fakeDomainService) Records(ctx context.Context, domain string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
	return f.recordsFunc(ctx, domain, listOpts)
}

func (f *fakeDomainService) Record(ctx context.Context, domain string, id int) (*godo.DomainRecord, *godo.Response, error) {
	return f.recordFunc(ctx, domain, id)
}

func (f *fakeDomainService) DeleteRecord(ctx context.Context, domain string, id int) (*godo.Response, error) {
	return f.deleteRecordFunc(ctx, domain, id)
}

func (f *fakeDomainService) EditRecord(ctx context.Context, domain string,
	id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
	return f.editRecordFunc(ctx, domain, id, editRequest)
}

func (f *fakeDomainService) CreateRecord(ctx context.Context, domain string,
	createRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
	return f.createRecordFunc(ctx, domain, createRequest)
}

func (f *fakeDomainService) RecordsByName(ctx context.Context, domain string, name string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
	return f.recordsByNameFunc(ctx, domain, name, listOpts)
}

func (f *fakeDomainService) RecordsByType(ctx context.Context, domain string, oftype string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
	return f.recordsByTypeFunc(ctx, domain, oftype, listOpts)
}

func (f *fakeDomainService) RecordsByTypeAndName(ctx context.Context, domain string, ofType string, name string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
	return f.recordsByTypeAndNameFunc(ctx, domain, ofType, name, listOpts)
}

func TestZonesList(t *testing.T) {
	client := godo.NewClient(nil)
	fake := &fakeDomainService{}

	// happy path
	fake.listFunc = func(ctx context.Context, listOpts *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
		domains := []godo.Domain{
			{
				Name: "example.com",
			},
		}

		return domains, nil, nil
	}
	client.Domains = fake

	z := &zones{client}
	zoneList, err := z.List()
	if err != nil {
		t.Errorf("error listing zones: %v", err)
	}

	if len(zoneList) != 1 {
		t.Errorf("expected only 1 zone, got %d", len(zoneList))
	}

	zone := zoneList[0]
	if zone.Name() != "example.com" {
		t.Errorf("expected example.com as zone name, got: %s", zone.Name())
	}

	// bad response path
	fake.listFunc = func(ctx context.Context, listOpts *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
		domains := []godo.Domain{
			{
				Name: "example.com",
			},
		}

		return domains, nil, errors.New("internal error!")
	}
	client.Domains = fake

	z = &zones{client}
	zoneList, err = z.List()
	if err == nil {
		t.Errorf("expected non-nil err")
	}

	if zoneList != nil {
		t.Errorf("expected nil zone, got %v", zoneList)
	}

	// godo client returned error path
	fake.listFunc = func(ctx context.Context, listOpts *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
		return nil, nil, errors.New("error!")
	}
	client.Domains = fake

	z = &zones{client}
	zoneList, err = z.List()
	if err == nil {
		t.Errorf("expected non-nil err")
	}

	if zoneList != nil {
		t.Errorf("expected nil zone, got %v", zoneList)
	}
}

func TestAdd(t *testing.T) {
	client := godo.NewClient(nil)
	fake := &fakeDomainService{}

	// happy path
	fake.createFunc = func(ctx context.Context, domainCreateRequest *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) {
		domain := &godo.Domain{Name: domainCreateRequest.Name}
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK

		return domain, resp, nil
	}
	client.Domains = fake

	dnsProvider := NewProvider(client)
	zs, _ := dnsProvider.Zones()
	inZone := &zone{name: "test", client: client}

	outZone, err := zs.Add(inZone)

	if outZone.Name() != "test" {
		t.Errorf("unexpected zone name: %s", outZone.Name())
	}

	if err != nil {
		t.Errorf("unexpected err: %v", err)
	}

	// bad status code
	fake.createFunc = func(ctx context.Context, domainCreateRequest *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) {
		domain := &godo.Domain{Name: domainCreateRequest.Name}

		return domain, nil, errors.New("bad response!")
	}
	client.Domains = fake

	dnsProvider = NewProvider(client)
	zs, _ = dnsProvider.Zones()
	inZone = &zone{name: "test", client: client}

	outZone, err = zs.Add(inZone)

	if outZone != nil {
		t.Errorf("expected zone to be nil, got :%v", outZone)
	}

	if err == nil {
		t.Errorf("expected non-nil err: %v", err)
	}

	// godo returns error
	fake.createFunc = func(ctx context.Context, domainCreateRequest *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) {
		domain := &godo.Domain{Name: domainCreateRequest.Name}

		return domain, nil, errors.New("error!")
	}
	client.Domains = fake

	dnsProvider = NewProvider(client)
	zs, _ = dnsProvider.Zones()
	inZone = &zone{name: "test", client: client}

	outZone, err = zs.Add(inZone)

	if outZone != nil {
		t.Errorf("expected zone to be nil, got :%v", outZone)
	}

	if err == nil {
		t.Errorf("expected non-nil err: %v", err)
	}
}

func TestRemove(t *testing.T) {
	client := godo.NewClient(nil)
	fake := &fakeDomainService{}

	// happy path
	fake.deleteFunc = func(ctx context.Context, name string) (*godo.Response, error) {
		return nil, nil
	}
	client.Domains = fake

	dnsProvider := NewProvider(client)
	zs, _ := dnsProvider.Zones()
	inZone := &zone{name: "test", client: client}

	err := zs.Remove(inZone)
	if err != nil {
		t.Errorf("unexpected err: %v", err)
	}

	// bad status code
	fake.deleteFunc = func(ctx context.Context, name string) (*godo.Response, error) {
		return nil, errors.New("bad response!")
	}
	client.Domains = fake

	dnsProvider = NewProvider(client)
	zs, _ = dnsProvider.Zones()
	inZone = &zone{name: "test", client: client}

	err = zs.Remove(inZone)
	if err == nil {
		t.Errorf("expected non-nil err: %v", err)
	}

	// godo returns error
	fake.deleteFunc = func(ctx context.Context, name string) (*godo.Response, error) {
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK
		resp.Body = io.NopCloser(bytes.NewBufferString("error!"))
		return resp, errors.New("error!")
	}
	client.Domains = fake

	dnsProvider = NewProvider(client)
	zs, _ = dnsProvider.Zones()
	inZone = &zone{name: "test", client: client}

	err = zs.Remove(inZone)
	if err == nil {
		t.Errorf("expected non-nil err: %v", err)
	}
}

func TestNewZone(t *testing.T) {
	client := godo.NewClient(nil)
	dnsprovider := NewProvider(client)
	zs, _ := dnsprovider.Zones()

	zone, err := zs.New("test")
	if err != nil {
		t.Errorf("error creating zone: %v", err)
	}

	if zone.Name() != "test" {
		t.Errorf("unexpected zone name: %v", zone.Name())
	}
}

func TestNewResourceRecordSet(t *testing.T) {
	fake := &fakeDomainService{}
	fake.recordsFunc = func(ctx context.Context, domain string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
		domainRecords := []godo.DomainRecord{
			{
				Name: "test",
				Data: "127.0.0.1",
				TTL:  3600,
				Type: "A",
			},
		}

		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK

		return domainRecords, resp, nil
	}
	client := godo.NewClient(nil)
	client.Domains = fake

	dnsprovider := NewProvider(client)
	zs, _ := dnsprovider.Zones()

	zone, err := zs.New("example.com")
	if err != nil {
		t.Errorf("error creating zone: %v", err)
	}

	if zone.Name() != "example.com" {
		t.Errorf("unexpected zone name: %v", zone.Name())
	}

	rrset, _ := zone.ResourceRecordSets()
	rrsets, err := rrset.List()
	if err != nil {
		t.Errorf("error listing resource record sets: %v", err)
	}

	if len(rrsets) != 1 {
		t.Errorf("unexpected number of records: %d", len(rrsets))
	}

	records, err := rrset.Get("test.example.com")
	if err != nil {
		t.Errorf("unexpected error getting resource record set: %v", err)
	}

	if len(records) != 1 {
		t.Errorf("unexpected records from resource record set: %d, expected 1 record", len(records))
	}

	if records[0].Name() != "test.example.com" {
		t.Errorf("unexpected record name: %s, expected 'test'", records[0].Name())
	}

	if len(records[0].Rrdatas()) != 1 {
		t.Errorf("unexpected number of resource record data: %d", len(records[0].Rrdatas()))
	}

	if records[0].Rrdatas()[0] != "127.0.0.1" {
		t.Errorf("unexpected resource record data: %s", records[0].Rrdatas()[0])
	}

	if records[0].Ttl() != 3600 {
		t.Errorf("unexpected record TTL: %d, expected 3600", records[0].Ttl())
	}

	if records[0].Type() != rrstype.A {
		t.Errorf("unexpected resource record type: %s, expected %s", records[0].Type(), rrstype.A)
	}
}

func TestResourceRecordChangeset(t *testing.T) {
	ctx := context.Background()

	fake := &fakeDomainService{}
	fake.recordsFunc = func(ctx context.Context, domain string, listOpts *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
		domainRecords := []godo.DomainRecord{
			{
				Name: "test",
				Data: "127.0.0.1",
				TTL:  3600,
				Type: "A",
			},
			{
				Name: "to-remove",
				Data: "127.0.0.1",
				TTL:  3600,
				Type: "A",
			},
			{
				Name: "to-upsert",
				Data: "127.0.0.1",
				TTL:  3600,
				Type: "A",
			},
		}

		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK

		return domainRecords, resp, nil
	}

	fake.createRecordFunc = func(ctx context.Context, domain string, createRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK
		return &godo.DomainRecord{}, resp, nil
	}

	fake.deleteRecordFunc = func(ctx context.Context, domain string, id int) (*godo.Response, error) {
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK
		return resp, nil
	}

	fake.editRecordFunc = func(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK
		return &godo.DomainRecord{}, resp, nil
	}

	fake.editRecordFunc = func(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
		resp := &godo.Response{
			Response: &http.Response{},
		}
		resp.StatusCode = http.StatusOK
		return &godo.DomainRecord{}, resp, nil
	}

	client := godo.NewClient(nil)
	client.Domains = fake

	dnsprovider := NewProvider(client)
	zs, _ := dnsprovider.Zones()

	zone, err := zs.New("example.com")
	if err != nil {
		t.Errorf("error creating zone: %v", err)
	}

	if zone.Name() != "example.com" {
		t.Errorf("unexpected zone name: %v", zone.Name())
	}

	rrset, _ := zone.ResourceRecordSets()

	changeset := rrset.StartChangeset()

	if !changeset.IsEmpty() {
		t.Error("expected empty changeset")
	}

	record := rrset.New("to-add", []string{"127.0.0.1"}, 3600, rrstype.A)
	changeset.Add(record)

	record = rrset.New("to-remove", []string{"127.0.0.1"}, 3600, rrstype.A)
	changeset.Remove(record)

	record = rrset.New("to-upsert", []string{"127.0.0.1"}, 3600, rrstype.A)
	changeset.Upsert(record)

	err = changeset.Apply(ctx)
	if err != nil {
		t.Errorf("error applying changeset: %v", err)
	}
}
