package dmapi

import (
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

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

const (
	correctAPIKey     = "123"
	incorrectAPIKey   = "321"
	serverErrorAPIKey = "500"
)

const (
	correctUsername     = "lego"
	incorrectUsername   = "not_lego"
	serverErrorUsername = "error"
)

func setup(t *testing.T) (*http.ServeMux, string) {
	t.Helper()

	mux := http.NewServeMux()

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

	return mux, server.URL
}

func TestDNSProvider_login_api_key(t *testing.T) {
	testCases := []struct {
		desc               string
		apiKey             string
		expectedError      bool
		expectedStatusCode int
		expectedAuthSid    string
	}{
		{
			desc:               "correct key",
			apiKey:             correctAPIKey,
			expectedStatusCode: 0,
			expectedAuthSid:    correctAPIKey,
		},
		{
			desc:               "incorrect key",
			apiKey:             incorrectAPIKey,
			expectedStatusCode: 2200,
			expectedError:      true,
		},
		{
			desc:               "server error",
			apiKey:             serverErrorAPIKey,
			expectedStatusCode: -500,
			expectedError:      true,
		},
		{
			desc:               "non-ok status code",
			apiKey:             "333",
			expectedStatusCode: 2202,
			expectedError:      true,
		},
	}

	mux, serverURL := setup(t)

	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)

		switch r.FormValue("api-key") {
		case correctAPIKey:
			_, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet")
		case incorrectAPIKey:
			_, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error")
		case serverErrorAPIKey:
			http.NotFound(w, r)
		default:
			_, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet")
		}
	})

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient(AuthInfo{APIKey: test.apiKey})
			client.BaseURL = serverURL

			response, err := client.Login()
			if test.expectedError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NotNil(t, response)
				assert.Equal(t, test.expectedStatusCode, response.StatusCode)
				assert.Equal(t, test.expectedAuthSid, response.AuthSid)
			}
		})
	}
}

func TestDNSProvider_login_username(t *testing.T) {
	testCases := []struct {
		desc               string
		username           string
		password           string
		expectedError      bool
		expectedStatusCode int
		expectedAuthSid    string
	}{
		{
			desc:               "correct username and password",
			username:           correctUsername,
			password:           "go-acme",
			expectedError:      false,
			expectedStatusCode: 0,
			expectedAuthSid:    correctAPIKey,
		},
		{
			desc:               "incorrect username",
			username:           incorrectUsername,
			password:           "go-acme",
			expectedStatusCode: 2200,
			expectedError:      true,
		},
		{
			desc:               "server error",
			username:           serverErrorUsername,
			password:           "go-acme",
			expectedStatusCode: -500,
			expectedError:      true,
		},
		{
			desc:               "non-ok status code",
			username:           "random",
			password:           "go-acme",
			expectedStatusCode: 2202,
			expectedError:      true,
		},
	}

	mux, serverURL := setup(t)

	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)

		switch r.FormValue("username") {
		case correctUsername:
			_, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet")
		case incorrectUsername:
			_, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error")
		case serverErrorUsername:
			http.NotFound(w, r)
		default:
			_, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet")
		}
	})

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient(AuthInfo{Username: test.username, Password: test.password})
			client.BaseURL = serverURL

			response, err := client.Login()
			if test.expectedError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NotNil(t, response)
				assert.Equal(t, test.expectedStatusCode, response.StatusCode)
				assert.Equal(t, test.expectedAuthSid, response.AuthSid)
			}
		})
	}
}

func TestDNSProvider_logout(t *testing.T) {
	testCases := []struct {
		desc               string
		authSid            string
		expectedError      bool
		expectedStatusCode int
	}{
		{
			desc:               "correct auth-sid",
			authSid:            correctAPIKey,
			expectedStatusCode: 0,
		},
		{
			desc:               "incorrect auth-sid",
			authSid:            incorrectAPIKey,
			expectedStatusCode: 2200,
		},
		{
			desc:          "already logged out",
			authSid:       "",
			expectedError: true,
		},
		{
			desc:          "server error",
			authSid:       serverErrorAPIKey,
			expectedError: true,
		},
	}

	mux, serverURL := setup(t)

	mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)

		switch r.FormValue("auth-sid") {
		case correctAPIKey:
			_, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n")
		case incorrectAPIKey:
			_, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error")
		default:
			http.NotFound(w, r)
		}
	})

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid})
			client.BaseURL = serverURL

			response, err := client.Logout()
			if test.expectedError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NotNil(t, response)
				assert.Equal(t, test.expectedStatusCode, response.StatusCode)
			}
		})
	}
}

func TestDNSProvider_getZone(t *testing.T) {
	testZone := "@ A 0 192.0.2.2 3600"

	testCases := []struct {
		desc               string
		authSid            string
		domain             string
		zone               string
		expectedError      bool
		expectedStatusCode int
	}{
		{
			desc:               "correct auth-sid, known domain",
			authSid:            correctAPIKey,
			domain:             "known",
			zone:               testZone,
			expectedStatusCode: 0,
		},
		{
			desc:               "incorrect auth-sid, known domain",
			authSid:            incorrectAPIKey,
			domain:             "known",
			expectedStatusCode: 2202,
		},
		{
			desc:               "correct auth-sid, unknown domain",
			authSid:            correctAPIKey,
			domain:             "unknown",
			expectedStatusCode: 2202,
		},
		{
			desc:          "server error",
			authSid:       "500",
			expectedError: true,
		},
	}

	mux, serverURL := setup(t)

	mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)

		authSid := r.FormValue("auth-sid")
		domain := r.FormValue("domain")

		switch {
		case authSid == correctAPIKey && domain == "known":
			_, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone)
		case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"):
			_, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: Authorization error")
		default:
			http.NotFound(w, r)
		}
	})

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid})
			client.BaseURL = serverURL

			response, err := client.GetZone(test.domain)
			if test.expectedError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NotNil(t, response)
				assert.Equal(t, test.expectedStatusCode, response.StatusCode)
				assert.Equal(t, test.zone, response.Body)
			}
		})
	}
}

func Test_parseResponse(t *testing.T) {
	testCases := []struct {
		desc     string
		input    string
		expected *Response
	}{
		{
			desc:  "Empty response",
			input: "",
			expected: &Response{
				Headers:    url.Values{},
				StatusCode: -1,
			},
		},
		{
			desc:  "No headers, just body",
			input: "\n\nTest body",
			expected: &Response{
				Headers:    url.Values{},
				Body:       "Test body",
				StatusCode: -1,
			},
		},
		{
			desc:  "Headers and body",
			input: "Test-Header: value\n\nTest body",
			expected: &Response{
				Headers:    url.Values{"Test-Header": {"value"}},
				Body:       "Test body",
				StatusCode: -1,
			},
		},
		{
			desc:  "Headers and body + Auth-Sid",
			input: "Test-Header: value\nAuth-Sid: 123\n\nTest body",
			expected: &Response{
				Headers:    url.Values{"Test-Header": {"value"}, "Auth-Sid": {"123"}},
				Body:       "Test body",
				StatusCode: -1,
				AuthSid:    "123",
			},
		},
		{
			desc:  "Headers and body + Status-Text",
			input: "Test-Header: value\nStatus-Text: OK\n\nTest body",
			expected: &Response{
				Headers:    url.Values{"Test-Header": {"value"}, "Status-Text": {"OK"}},
				Body:       "Test body",
				StatusText: "OK",
				StatusCode: -1,
			},
		},
		{
			desc:  "Headers and body + Status-Code",
			input: "Test-Header: value\nStatus-Code: 2020\n\nTest body",
			expected: &Response{
				Headers:    url.Values{"Test-Header": {"value"}, "Status-Code": {"2020"}},
				Body:       "Test body",
				StatusCode: 2020,
			},
		},
	}

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

			response := parseResponse(test.input)

			assert.Equal(t, test.expected, response)
		})
	}
}

func Test_removeTxtEntryFromZone(t *testing.T) {
	testCases := []struct {
		desc     string
		input    string
		expected string
		modified bool
	}{
		{
			desc:     "empty zone",
			input:    "",
			expected: "",
			modified: false,
		},
		{
			desc:     "zone with only A entry",
			input:    "@ A 0 192.0.2.2 3600",
			expected: "@ A 0 192.0.2.2 3600",
			modified: false,
		},
		{
			desc:     "zone with only clenup entry",
			input:    "_acme-challenge TXT 0  \"old \" 120",
			expected: "",
			modified: true,
		},
		{
			desc:     "zone with one A and one cleanup entries",
			input:    "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0  \"old \" 120",
			expected: "@ A 0 192.0.2.2 3600",
			modified: true,
		},
		{
			desc:     "zone with one A and multiple cleanup entries",
			input:    "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0  \"old \" 120\n_acme-challenge TXT 0  \"another \" 120",
			expected: "@ A 0 192.0.2.2 3600",
			modified: true,
		},
	}

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

			zone, modified := RemoveTxtEntryFromZone(test.input, "_acme-challenge")
			assert.Equal(t, zone, test.expected)
			assert.Equal(t, modified, test.modified)
		})
	}
}

func Test_addTxtEntryToZone(t *testing.T) {
	testCases := []struct {
		desc     string
		input    string
		expected string
	}{
		{
			desc:     "empty zone",
			input:    "",
			expected: "_acme-challenge TXT 0 \"test\" 120",
		},
		{
			desc:     "zone with A entry",
			input:    "@ A 0 192.0.2.2 3600",
			expected: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"test\" 120",
		},
		{
			desc:     "zone with required cleanup entry",
			input:    "_acme-challenge TXT 0  \"old \" 120",
			expected: "_acme-challenge TXT 0 \"old\" 120\n_acme-challenge TXT 0 \"test\" 120",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			zone := AddTxtEntryToZone(test.input, "_acme-challenge", "test", 120)
			assert.Equal(t, zone, test.expected)
		})
	}
}

func Test_fixTxtLines(t *testing.T) {
	testCases := []struct {
		desc     string
		input    string
		expected string
	}{
		{
			desc:     "clean-up",
			input:    `_acme-challenge TXT 0  "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE " 120`,
			expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`,
		},
		{
			desc:     "already cleaned",
			input:    `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`,
			expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`,
		},
		{
			desc:     "special DNS entry",
			input:    "$dyndns=yes:username:password",
			expected: "$dyndns=yes:username:password",
		},
		{
			desc:     "SRV entry",
			input:    "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300",
			expected: "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300",
		},
		{
			desc:     "MX entry",
			input:    "@ MX 10 ASPMX.L.GOOGLE.COM 300",
			expected: "@ MX 10 ASPMX.L.GOOGLE.COM 300",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			line := fixTxtLines(test.input)
			assert.Equal(t, line, test.expected)
		})
	}
}
