package internal

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"

	"github.com/go-acme/lego/v4/challenge/dns01"
	"github.com/go-acme/lego/v4/platform/tester"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var envTest = tester.NewEnvTest(
	"NETCUP_CUSTOMER_NUMBER",
	"NETCUP_API_KEY",
	"NETCUP_API_PASSWORD").
	WithDomain("NETCUP_DOMAIN")

func setupClientTest() (*Client, *http.ServeMux, func()) {
	handler := http.NewServeMux()
	server := httptest.NewServer(handler)

	client, err := NewClient("a", "b", "c")
	if err != nil {
		panic(err)
	}
	client.BaseURL = server.URL

	return client, handler, server.Close
}

func TestGetDNSRecordIdx(t *testing.T) {
	records := []DNSRecord{
		{
			ID:           12345,
			Hostname:     "asdf",
			RecordType:   "TXT",
			Priority:     "0",
			Destination:  "randomtext",
			DeleteRecord: false,
			State:        "yes",
		},
		{
			ID:           23456,
			Hostname:     "@",
			RecordType:   "A",
			Priority:     "0",
			Destination:  "127.0.0.1",
			DeleteRecord: false,
			State:        "yes",
		},
		{
			ID:           34567,
			Hostname:     "dfgh",
			RecordType:   "CNAME",
			Priority:     "0",
			Destination:  "example.com",
			DeleteRecord: false,
			State:        "yes",
		},
		{
			ID:           45678,
			Hostname:     "fghj",
			RecordType:   "MX",
			Priority:     "10",
			Destination:  "mail.example.com",
			DeleteRecord: false,
			State:        "yes",
		},
	}

	testCases := []struct {
		desc        string
		record      DNSRecord
		expectError bool
	}{
		{
			desc: "simple",
			record: DNSRecord{
				ID:           12345,
				Hostname:     "asdf",
				RecordType:   "TXT",
				Priority:     "0",
				Destination:  "randomtext",
				DeleteRecord: false,
				State:        "yes",
			},
		},
		{
			desc: "wrong Destination",
			record: DNSRecord{
				ID:           12345,
				Hostname:     "asdf",
				RecordType:   "TXT",
				Priority:     "0",
				Destination:  "wrong",
				DeleteRecord: false,
				State:        "yes",
			},
			expectError: true,
		},
		{
			desc: "record type CNAME",
			record: DNSRecord{
				ID:           12345,
				Hostname:     "asdf",
				RecordType:   "CNAME",
				Priority:     "0",
				Destination:  "randomtext",
				DeleteRecord: false,
				State:        "yes",
			},
			expectError: true,
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			idx, err := GetDNSRecordIdx(records, test.record)
			if test.expectError {
				assert.Error(t, err)
				assert.Equal(t, -1, idx)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, records[idx], test.record)
			}
		})
	}
}

func TestClient_Login(t *testing.T) {
	client, mux, tearDown := setupClientTest()
	defer tearDown()

	mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
		raw, err := ioutil.ReadAll(req.Body)
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}

		if string(raw) != `{"action":"login","param":{"customernumber":"a","apikey":"b","apipassword":"c"}}` {
			http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
		}

		response := `
		{
		    "serverrequestid": "srv-request-id",
		    "clientrequestid": "",
		    "action": "login",
		    "status": "success",
		    "statuscode": 2000,
		    "shortmessage": "Login successful",
		    "longmessage": "Session has been created successful.",
		    "responsedata": {
		        "apisessionid": "api-session-id"
		    }
		}
		`
		_, err = rw.Write([]byte(response))
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}
	})

	sessionID, err := client.Login()
	require.NoError(t, err)

	assert.Equal(t, "api-session-id", sessionID)
}

func TestClient_Login_errors(t *testing.T) {
	testCases := []struct {
		desc    string
		handler func(rw http.ResponseWriter, req *http.Request)
	}{
		{
			desc: "HTTP error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				http.Error(rw, "error message", http.StatusInternalServerError)
			},
		},
		{
			desc: "API error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				response := `
					{
						"serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
						"clientrequestid":"",
						"action":"login",
						"status":"error",
						"statuscode":4013,
						"shortmessage":"Validation Error.",
						"longmessage":"Message is empty.",
						"responsedata":""
					}`
				_, err := rw.Write([]byte(response))
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}
			},
		},
		{
			desc: "responsedata marshaling error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				response := `
							{
								"serverrequestid": "srv-request-id",
								"clientrequestid": "",
								"action": "login",
								"status": "success",
								"statuscode": 2000,
								"shortmessage": "Login successful",
								"longmessage": "Session has been created successful.",
								"responsedata": ""
							}`
				_, err := rw.Write([]byte(response))
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			client, mux, tearDown := setupClientTest()
			defer tearDown()

			mux.HandleFunc("/", test.handler)

			sessionID, err := client.Login()
			assert.Error(t, err)
			assert.Equal(t, "", sessionID)
		})
	}
}

func TestClient_Logout(t *testing.T) {
	client, mux, tearDown := setupClientTest()
	defer tearDown()

	mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
		raw, err := ioutil.ReadAll(req.Body)
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}

		if string(raw) != `{"action":"logout","param":{"customernumber":"a","apikey":"b","apisessionid":"session-id"}}` {
			http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
		}

		response := `
			{
				"serverrequestid": "request-id",
				"clientrequestid": "",
				"action": "logout",
				"status": "success",
				"statuscode": 2000,
				"shortmessage": "Logout successful",
				"longmessage": "Session has been terminated successful.",
				"responsedata": ""
			}`
		_, err = rw.Write([]byte(response))
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}
	})

	err := client.Logout("session-id")
	require.NoError(t, err)
}

func TestClient_Logout_errors(t *testing.T) {
	testCases := []struct {
		desc    string
		handler func(rw http.ResponseWriter, req *http.Request)
	}{
		{
			desc: "HTTP error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				http.Error(rw, "error message", http.StatusInternalServerError)
			},
		},
		{
			desc: "API error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				response := `
					{
						"serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
						"clientrequestid":"",
						"action":"logout",
						"status":"error",
						"statuscode":4013,
						"shortmessage":"Validation Error.",
						"longmessage":"Message is empty.",
						"responsedata":""
					}`
				_, err := rw.Write([]byte(response))
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			client, mux, tearDown := setupClientTest()
			defer tearDown()

			mux.HandleFunc("/", test.handler)

			err := client.Logout("session-id")
			require.Error(t, err)
		})
	}
}

func TestClient_GetDNSRecords(t *testing.T) {
	client, mux, tearDown := setupClientTest()
	defer tearDown()

	mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
		raw, err := ioutil.ReadAll(req.Body)
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}

		if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` {
			http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
		}

		response := `
			{
			  "serverrequestid":"srv-request-id",
			  "clientrequestid":"",
			  "action":"infoDnsRecords",
			  "status":"success",
			  "statuscode":2000,
			  "shortmessage":"Login successful",
			  "longmessage":"Session has been created successful.",
			  "responsedata":{
			    "apisessionid":"api-session-id",
			    "dnsrecords":[
			      {
			        "id":"1",
			        "hostname":"example.com",
			        "type":"TXT",
			        "priority":"1",
			        "destination":"bGVnbzE=",
			        "state":"yes",
			        "ttl":300
			      },
			      {
			        "id":"2",
			        "hostname":"example2.com",
			        "type":"TXT",
			        "priority":"1",
			        "destination":"bGVnbw==",
			        "state":"yes",
			        "ttl":300
			      }
			    ]
			  }
			}`
		_, err = rw.Write([]byte(response))
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
		}
	})

	expected := []DNSRecord{{
		ID:           1,
		Hostname:     "example.com",
		RecordType:   "TXT",
		Priority:     "1",
		Destination:  "bGVnbzE=",
		DeleteRecord: false,
		State:        "yes",
		TTL:          300,
	}, {
		ID:           2,
		Hostname:     "example2.com",
		RecordType:   "TXT",
		Priority:     "1",
		Destination:  "bGVnbw==",
		DeleteRecord: false,
		State:        "yes",
		TTL:          300,
	}}

	records, err := client.GetDNSRecords("example.com", "api-session-id")
	require.NoError(t, err)

	assert.Equal(t, expected, records)
}

func TestClient_GetDNSRecords_errors(t *testing.T) {
	testCases := []struct {
		desc    string
		handler func(rw http.ResponseWriter, req *http.Request)
	}{
		{
			desc: "HTTP error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				http.Error(rw, "error message", http.StatusInternalServerError)
			},
		},
		{
			desc: "API error",
			handler: func(rw http.ResponseWriter, _ *http.Request) {
				response := `
					{
						"serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
						"clientrequestid":"",
						"action":"infoDnsRecords",
						"status":"error",
						"statuscode":4013,
						"shortmessage":"Validation Error.",
						"longmessage":"Message is empty.",
						"responsedata":""
					}`
				_, err := rw.Write([]byte(response))
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}
			},
		},
		{
			desc: "responsedata marshaling error",
			handler: func(rw http.ResponseWriter, req *http.Request) {
				raw, err := ioutil.ReadAll(req.Body)
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}

				if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` {
					http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
				}

				response := `
			{
			  "serverrequestid":"srv-request-id",
			  "clientrequestid":"",
			  "action":"infoDnsRecords",
			  "status":"success",
			  "statuscode":2000,
			  "shortmessage":"Login successful",
			  "longmessage":"Session has been created successful.",
			  "responsedata":""
			}`
				_, err = rw.Write([]byte(response))
				if err != nil {
					http.Error(rw, err.Error(), http.StatusInternalServerError)
				}
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			client, mux, tearDown := setupClientTest()
			defer tearDown()

			mux.HandleFunc("/", test.handler)

			records, err := client.GetDNSRecords("example.com", "api-session-id")
			require.Error(t, err)
			assert.Empty(t, records)
		})
	}
}

func TestLiveClientAuth(t *testing.T) {
	if !envTest.IsLiveTest() {
		t.Skip("skipping live test")
	}

	// Setup
	envTest.RestoreEnv()

	client, err := NewClient(
		envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
		envTest.GetValue("NETCUP_API_KEY"),
		envTest.GetValue("NETCUP_API_PASSWORD"))
	require.NoError(t, err)

	for i := 1; i < 4; i++ {
		i := i
		t.Run("Test_"+strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			sessionID, err := client.Login()
			require.NoError(t, err)

			err = client.Logout(sessionID)
			require.NoError(t, err)
		})
	}
}

func TestLiveClientGetDnsRecords(t *testing.T) {
	if !envTest.IsLiveTest() {
		t.Skip("skipping live test")
	}

	// Setup
	envTest.RestoreEnv()

	client, err := NewClient(
		envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
		envTest.GetValue("NETCUP_API_KEY"),
		envTest.GetValue("NETCUP_API_PASSWORD"))
	require.NoError(t, err)

	sessionID, err := client.Login()
	require.NoError(t, err)

	fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==")

	zone, err := dns01.FindZoneByFqdn(fqdn)
	require.NoError(t, err, "error finding DNSZone")

	zone = dns01.UnFqdn(zone)

	// TestMethod
	_, err = client.GetDNSRecords(zone, sessionID)
	require.NoError(t, err)

	// Tear down
	err = client.Logout(sessionID)
	require.NoError(t, err)
}

func TestLiveClientUpdateDnsRecord(t *testing.T) {
	if !envTest.IsLiveTest() {
		t.Skip("skipping live test")
	}

	// Setup
	envTest.RestoreEnv()

	client, err := NewClient(
		envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
		envTest.GetValue("NETCUP_API_KEY"),
		envTest.GetValue("NETCUP_API_PASSWORD"))
	require.NoError(t, err)

	sessionID, err := client.Login()
	require.NoError(t, err)

	fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==")

	zone, err := dns01.FindZoneByFqdn(fqdn)
	require.NoError(t, err, fmt.Errorf("error finding DNSZone, %w", err))

	hostname := strings.Replace(fqdn, "."+zone, "", 1)

	record := DNSRecord{
		Hostname:     hostname,
		RecordType:   "TXT",
		Destination:  "asdf5678",
		DeleteRecord: false,
		TTL:          120,
	}

	// test
	zone = dns01.UnFqdn(zone)

	err = client.UpdateDNSRecord(sessionID, zone, []DNSRecord{record})
	require.NoError(t, err)

	records, err := client.GetDNSRecords(zone, sessionID)
	require.NoError(t, err)

	recordIdx, err := GetDNSRecordIdx(records, record)
	require.NoError(t, err)

	assert.Equal(t, record.Hostname, records[recordIdx].Hostname)
	assert.Equal(t, record.RecordType, records[recordIdx].RecordType)
	assert.Equal(t, record.Destination, records[recordIdx].Destination)
	assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord)

	records[recordIdx].DeleteRecord = true

	// Tear down
	err = client.UpdateDNSRecord(sessionID, envTest.GetDomain(), []DNSRecord{records[recordIdx]})
	require.NoError(t, err, "Did not remove record! Please do so yourself.")

	err = client.Logout(sessionID)
	require.NoError(t, err)
}