package loopia

import (
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

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

func TestClient_CreateRecord(t *testing.T) {
	serverResponses := map[string]string{
		addZoneRecordGoodAuth:       responseOk,
		addZoneRecordBadAuth:        responseAuthError,
		addZoneRecordNonValidDomain: responseUnknownError,
		addZoneRecordEmptyResponse:  "",
	}

	serverURL := createFakeServer(t, serverResponses)

	testCases := []struct {
		desc     string
		password string
		domain   string
		err      string
	}{
		{
			desc:     "auth ok",
			password: "goodpassword",
			domain:   exampleDomain,
		},
		{
			desc:     "auth error",
			password: "badpassword",
			domain:   exampleDomain,
			err:      "authentication error",
		},
		{
			desc:     "unknown error",
			password: "goodpassword",
			domain:   "badexample.com",
			err:      `unknown error: "UNKNOWN_ERROR"`,
		},
		{
			desc:     "empty response",
			password: "goodpassword",
			domain:   "empty.com",
			err:      "error unmarshalling the API response XML body: EOF",
		},
	}

	zr := zRec{
		Type:     "TXT",
		TTL:      123,
		Rdata:    "TXTrecord",
		RecordID: 0,
		Priority: 0,
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient("apiuser", test.password, "", false, true, false)
			client.BaseURL = serverURL + "/"

			err := client.CreateRecord(test.domain, exampleSubDomain, zr.SetPS())
			if test.err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				assert.EqualError(t, err, test.err)
			}
		})
	}
}

func TestClient_DeleteSubdomain(t *testing.T) {
	serverResponses := map[string]string{
		removeSubdomainGoodAuth:       responseOk,
		removeSubdomainBadAuth:        responseAuthError,
		removeSubdomainNonValidDomain: responseUnknownError,
		removeSubdomainEmptyResponse:  "",
	}

	serverURL := createFakeServer(t, serverResponses)

	testCases := []struct {
		desc     string
		password string
		domain   string
		err      string
	}{
		{
			desc:     "auth ok",
			password: "goodpassword",
			domain:   exampleDomain,
		},
		{
			desc:     "auth error",
			password: "badpassword",
			domain:   exampleDomain,
			err:      "authentication error",
		},
		{
			desc:     "unknown error",
			password: "goodpassword",
			domain:   "badexample.com",
			err:      `unknown error: "UNKNOWN_ERROR"`,
		},
		{
			desc:     "empty response",
			password: "goodpassword",
			domain:   "empty.com",
			err:      "error unmarshalling the API response XML body: EOF",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient("apiuser", test.password, "", false, true, false)
			client.BaseURL = serverURL + "/"

			err := client.DeleteSubdomain(test.domain, exampleSubDomain)
			if test.err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				assert.EqualError(t, err, test.err)
			}
		})
	}
}

func TestClient_DeleteRecord(t *testing.T) {
	serverResponses := map[string]string{
		removeRecordGoodAuth:       responseOk,
		removeRecordBadAuth:        responseAuthError,
		removeRecordNonValidDomain: responseUnknownError,
		removeRecordEmptyResponse:  "",
	}

	serverURL := createFakeServer(t, serverResponses)

	testCases := []struct {
		desc     string
		password string
		domain   string
		err      string
	}{
		{
			desc:     "auth ok",
			password: "goodpassword",
			domain:   exampleDomain,
		},
		{
			desc:     "auth error",
			password: "badpassword",
			domain:   exampleDomain,
			err:      "authentication error",
		},
		{
			desc:     "uknown error",
			password: "goodpassword",
			domain:   "badexample.com",
			err:      `unknown error: "UNKNOWN_ERROR"`,
		},
		{
			desc:     "empty response",
			password: "goodpassword",
			domain:   "empty.com",
			err:      "error unmarshalling the API response XML body: EOF",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient("apiuser", test.password, "", false, true, false)
			client.BaseURL = serverURL + "/"

			err := client.DeleteRecord(test.domain, exampleSubDomain, 12345678)
			if test.err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				assert.EqualError(t, err, test.err)
			}
		})
	}
}

func TestClient_GetDomainRecords(t *testing.T) {
	serverResponses := map[string]string{
		getZoneRecords: getZoneRecordsResponse,
	}

	serverURL := createFakeServer(t, serverResponses)

	client := NewClient("apiuser", "goodpassword", "", false, true, false)
	client.BaseURL = serverURL + "/"

	recordObjs, err := client.getDomainRecords(exampleDomain, exampleSubDomain)
	require.NoError(t, err)

	zr := zRec{
		Type:     "TXT",
		TTL:      300,
		Priority: 0,
		Rdata:    exampleRdata,
		RecordID: 12345678,
	}
	expected := zr.SetZR()
	var zrs []zoneRecord
	zrs = append(zrs, expected)

	assert.EqualValues(t, zrs, recordObjs)
}

func TestClient_rpcCall_404(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		w.WriteHeader(http.StatusNotFound)

		_, err = fmt.Fprint(w, "<?xml version='1.0' encoding='UTF-8'?>")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}))

	t.Cleanup(server.Close)

	call := &methodCall{
		MethodName: "dummyMethod",
		Params: []param{
			paramString{Value: "test1"},
		},
	}

	client := NewClient("apiuser", "apipassword", "", false, true, false)
	client.BaseURL = server.URL + "/"

	err := client.rpcCall(call, &responseString{})
	assert.EqualError(t, err, "HTTP Post Error: 404")
}

func TestClient_rpcCall_RPCError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		_, err = fmt.Fprint(w, responseRPCError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}))

	t.Cleanup(server.Close)

	call := &methodCall{
		MethodName: "getDomains",
		Params: []param{
			paramString{Value: "test1"},
		},
	}

	client := NewClient("apiuser", "apipassword", "", false, true, false)
	client.BaseURL = server.URL + "/"

	err := client.rpcCall(call, &responseString{})
	assert.EqualError(t, err, "RPC Error: (201) Method signature error: 42")
}

func TestUnmarshallFaultyRecordObject(t *testing.T) {
	testCases := []struct {
		desc string
		xml  string
	}{
		{
			desc: "faulty name",
			xml:  "<name>name<name>",
		},
		{
			desc: "faulty string",
			xml:  "<value><string>foo<string></value>",
		},
		{
			desc: "faulty int",
			xml:  "<value><int>1<int></value>",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			resp := &zoneRecord{}

			err := xml.Unmarshal([]byte(test.xml), resp)
			require.Error(t, err)
		})
	}
}

func createFakeServer(t *testing.T, serverResponses map[string]string) string {
	t.Helper()

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Content-Type") != "text/xml" {
			http.Error(w, "invalid content type: "+r.Header.Get("Content-Type"), http.StatusBadRequest)
			return
		}

		req, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		resp, ok := serverResponses[string(req)]
		if !ok {
			http.Error(w, "no response for request", http.StatusBadRequest)
			return
		}

		_, err = fmt.Fprint(w, resp)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})

	server := httptest.NewServer(handler)
	t.Cleanup(server.Close)

	return server.URL
}
