package ifttt

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"goa.design/clue/log"
)

const (
	baseURL      = "https://maker.ifttt.com"
	presentEvent = "presence_detected"
	absentEvent  = "absence_detected"
)

var (
	presentValues = Values{
		Value1: "presence_detected_value1",
		Value2: "presence_detected_value2",
		Value3: "presence_detected_value3",
	}
	absentValues = Values{
		Value1: "absence_detected_value1",
		Value2: "absence_detected_value2",
		Value3: "absence_detected_value3",
	}
)

func TestNewClient(t *testing.T) {
	t.Run("invalid base URL", func(t *testing.T) {
		_, err := NewClient(http.DefaultClient, "%", "key", presentEvent, absentEvent, presentValues, absentValues, false)
		assert.ErrorContains(t, err, `parse "%": invalid URL escape "%"`)
	})
}

func TestClient_Trigger(t *testing.T) {
	ctx := log.Context(context.Background(), log.WithDebug())

	cases := []struct {
		name, key, event, err string
		ctx                   context.Context
		present, noDebug      bool
		values                Values
		handler               func(t *testing.T) http.HandlerFunc
	}{
		{
			name:    "present",
			key:     "key",
			ctx:     ctx,
			present: true,
			values:  presentValues,
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					assert := assert.New(t)

					assert.Equal(http.MethodPost, r.Method)
					assert.Equal("/trigger/"+presentEvent+"/with/key/key", r.URL.Path)

					body, err := io.ReadAll(r.Body)
					assert.NoError(err)
					assert.JSONEq(`{
						"value1": "presence_detected_value1",
						"value2": "presence_detected_value2",
						"value3": "presence_detected_value3"
					}`, string(body))
				}
			},
			event: presentEvent,
		},
		{
			name:    "absent",
			key:     "key",
			ctx:     ctx,
			present: false,
			values:  absentValues,
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					assert := assert.New(t)

					assert.Equal(http.MethodPost, r.Method)
					assert.Equal("/trigger/"+absentEvent+"/with/key/key", r.URL.Path)

					body, err := io.ReadAll(r.Body)
					assert.NoError(err)
					assert.JSONEq(`{
						"value1": "absence_detected_value1",
						"value2": "absence_detected_value2",
						"value3": "absence_detected_value3"
					}`, string(body))
				}
			},
			event: absentEvent,
		},
		{
			name: "nil context",
			ctx:  nil,
			key:  "key",
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {}
			},
			err: "net/http: nil Context",
		},
		{
			name: "closed connection",
			ctx:  ctx,
			key:  "key",
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					assert := assert.New(t)

					hj, ok := w.(http.Hijacker)
					if !assert.Equal(true, ok) {
						assert.FailNow("server doesn't support hijacking")
					}

					conn, _, err := hj.Hijack()
					if !assert.NoError(err) {
						assert.FailNow("error hijacking")
					}

					assert.NoError(conn.Close())
				}
			},
			err: `Post "` + baseURL + `/trigger/` + absentEvent + `/with/key/key": EOF`,
		},
		{
			name: "unauthorized",
			ctx:  ctx,
			key:  "key",
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusUnauthorized)
					_, _ = w.Write([]byte(`{"errors":[{"message":"You sent an invalid key."}]}`))
				}
			},
			err: `401 Unauthorized: {"errors":[{"message":"You sent an invalid key."}]}`,
		},
		{
			name: "empty body",
			ctx:  ctx,
			key:  "key",
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusInternalServerError)
				}
			},
			err: "500 Internal Server Error: <empty body>",
		},
		{
			name:    "failed to read body",
			ctx:     ctx,
			key:     "key",
			noDebug: true, // goahttp.DebugDoer interferes with this test
			handler: func(t *testing.T) http.HandlerFunc {
				return func(w http.ResponseWriter, r *http.Request) {
					w.Header().Set("Content-Length", "1")
					w.WriteHeader(http.StatusBadGateway)
				}
			},
			err: "502 Bad Gateway: <failed to read body: unexpected EOF>",
		},
	}

	for _, tc := range cases {
		tc := tc

		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			assert := assert.New(t)

			ts := httptest.NewTLSServer(tc.handler(t))
			defer ts.Close()

			c, err := NewClient(ts.Client(), ts.URL, tc.key, presentEvent, absentEvent, presentValues, absentValues, !tc.noDebug)
			assert.NoError(err)

			event, values, err := c.Trigger(tc.ctx, tc.present)
			if tc.err != "" {
				tc.err = strings.ReplaceAll(tc.err, baseURL, ts.URL)
				assert.EqualError(err, tc.err)
			} else if assert.NoError(err) {
				assert.Equal(tc.event, event)
				assert.Equal(&tc.values, values)
			}
		})
	}
}