package acmedns

import (
	"errors"
	"testing"

	"github.com/cpu/goacmedns"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	// errorClientErr is used by the Client mocks that return an error.
	errorClientErr = errors.New("errorClient always errors")
	// errorStorageErr is used by the Storage mocks that return an error.
	errorStorageErr = errors.New("errorStorage always errors")
)

const (
	// Fixed test data for unit tests.
	egDomain  = "threeletter.agency"
	egFQDN    = "_acme-challenge." + egDomain + "."
	egKeyAuth = "⚷"
)

var egTestAccount = goacmedns.Account{
	FullDomain: "acme-dns." + egDomain,
	SubDomain:  "random-looking-junk." + egDomain,
	Username:   "spooky.mulder",
	Password:   "trustno1",
}

// mockClient is a mock implementing the acmeDNSClient interface that always
// returns a fixed goacmedns.Account from calls to Register.
type mockClient struct {
	mockAccount goacmedns.Account
}

// UpdateTXTRecord does nothing.
func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
	return nil
}

// RegisterAccount returns c.mockAccount and no errors.
func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
	return c.mockAccount, nil
}

// mockUpdateClient is a mock implementing the acmeDNSClient interface that
// tracks the calls to UpdateTXTRecord in the records map.
type mockUpdateClient struct {
	mockClient
	records map[goacmedns.Account]string
}

// UpdateTXTRecord saves a record value to c.records for the given acct.
func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error {
	c.records[acct] = value
	return nil
}

// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
// returns errors from errorUpdateClient.
type errorUpdateClient struct {
	mockClient
}

// UpdateTXTRecord always returns an error.
func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
	return errorClientErr
}

// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
// returns errors from RegisterAccount.
type errorRegisterClient struct {
	mockClient
}

// RegisterAccount always returns an error.
func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
	return goacmedns.Account{}, errorClientErr
}

// mockStorage is a mock implementing the goacmedns.Storage interface that
// returns static account data and ignores Save.
type mockStorage struct {
	accounts map[string]goacmedns.Account
}

// Save does nothing.
func (m mockStorage) Save() error {
	return nil
}

// Put stores an account for the given domain in m.accounts.
func (m mockStorage) Put(domain string, acct goacmedns.Account) error {
	m.accounts[domain] = acct
	return nil
}

// Fetch retrieves an account for the given domain from m.accounts or returns
// goacmedns.ErrDomainNotFound.
func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) {
	if acct, ok := m.accounts[domain]; ok {
		return acct, nil
	}
	return goacmedns.Account{}, goacmedns.ErrDomainNotFound
}

// errorPutStorage is a mock implementing the goacmedns.Storage interface that
// always returns errors from Put.
type errorPutStorage struct {
	mockStorage
}

// Put always errors.
func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error {
	return errorStorageErr
}

// errorSaveStorage is a mock implementing the goacmedns.Storage interface that
// always returns errors from Save.
type errorSaveStorage struct {
	mockStorage
}

// Save always errors.
func (e errorSaveStorage) Save() error {
	return errorStorageErr
}

// errorFetchStorage is a mock implementing the goacmedns.Storage interface that
// always returns errors from Fetch.
type errorFetchStorage struct {
	mockStorage
}

// Fetch always errors.
func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) {
	return goacmedns.Account{}, errorStorageErr
}

// TestPresent tests that the ACME-DNS Present function for updating a DNS-01
// challenge response TXT record works as expected.
func TestPresent(t *testing.T) {
	// validAccountStorage is a mockStorage configured to return the egTestAccount.
	validAccountStorage := mockStorage{
		map[string]goacmedns.Account{
			egDomain: egTestAccount,
		},
	}
	// validUpdateClient is a mockClient configured with the egTestAccount that will
	// track TXT updates in a map.
	validUpdateClient := mockUpdateClient{
		mockClient{egTestAccount},
		make(map[goacmedns.Account]string),
	}

	testCases := []struct {
		Name          string
		Client        acmeDNSClient
		Storage       goacmedns.Storage
		ExpectedError error
	}{
		{
			Name:          "present when client storage returns unexpected error",
			Client:        mockClient{egTestAccount},
			Storage:       errorFetchStorage{},
			ExpectedError: errorStorageErr,
		},
		{
			Name:   "present when client storage returns ErrDomainNotFound",
			Client: mockClient{egTestAccount},
			ExpectedError: ErrCNAMERequired{
				Domain: egDomain,
				FQDN:   egFQDN,
				Target: egTestAccount.FullDomain,
			},
		},
		{
			Name:          "present when client UpdateTXTRecord returns unexpected error",
			Client:        errorUpdateClient{},
			Storage:       validAccountStorage,
			ExpectedError: errorClientErr,
		},
		{
			Name:    "present when everything works",
			Storage: validAccountStorage,
			Client:  validUpdateClient,
		},
	}

	for _, test := range testCases {
		t.Run(test.Name, func(t *testing.T) {
			dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)})
			require.NoError(t, err)

			// override the storage mock if required by the test case.
			if test.Storage != nil {
				dp.storage = test.Storage
			}

			// call Present. The token argument can be garbage because the ACME-DNS
			// provider does not use it.
			err = dp.Present(egDomain, "foo", egKeyAuth)
			if test.ExpectedError != nil {
				assert.Equal(t, test.ExpectedError, err)
			} else {
				require.NoError(t, err)
			}
		})
	}

	// Check that the success test case set a record.
	assert.Len(t, validUpdateClient.records, 1)

	// Check that the success test case set the right record for the right account.
	assert.Len(t, validUpdateClient.records[egTestAccount], 43)
}

// TestRegister tests that the ACME-DNS register function works correctly.
func TestRegister(t *testing.T) {
	testCases := []struct {
		Name          string
		Client        acmeDNSClient
		Storage       goacmedns.Storage
		Domain        string
		FQDN          string
		ExpectedError error
	}{
		{
			Name:          "register when acme-dns client returns an error",
			Client:        errorRegisterClient{},
			ExpectedError: errorClientErr,
		},
		{
			Name:          "register when acme-dns storage put returns an error",
			Client:        mockClient{egTestAccount},
			Storage:       errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}},
			ExpectedError: errorStorageErr,
		},
		{
			Name:          "register when acme-dns storage save returns an error",
			Client:        mockClient{egTestAccount},
			Storage:       errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}},
			ExpectedError: errorStorageErr,
		},
		{
			Name:   "register when everything works",
			Client: mockClient{egTestAccount},
			ExpectedError: ErrCNAMERequired{
				Domain: egDomain,
				FQDN:   egFQDN,
				Target: egTestAccount.FullDomain,
			},
		},
	}

	for _, test := range testCases {
		t.Run(test.Name, func(t *testing.T) {
			dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)})
			require.NoError(t, err)

			// override the storage mock if required by the testcase.
			if test.Storage != nil {
				dp.storage = test.Storage
			}

			// Call register for the example domain/fqdn.
			err = dp.register(egDomain, egFQDN)
			if test.ExpectedError != nil {
				assert.Equal(t, test.ExpectedError, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}
