// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/alertmanager/config"
	"github.com/prometheus/alertmanager/pkg/labels"
	"github.com/prometheus/alertmanager/types"
)

type apiTest struct {
	// Wrapper around the tested function.
	do func() (interface{}, error)

	apiRes fakeAPIResponse

	// Expected values returned by the tested function.
	res interface{}
	err error
}

// Fake HTTP client for TestAPI.
type fakeAPIClient struct {
	*testing.T
	ch chan fakeAPIResponse
}

type fakeAPIResponse struct {
	// Expected input values.
	path   string
	method string

	// Values to be returned by fakeAPIClient.Do().
	err error
	res interface{}
}

func (c *fakeAPIClient) URL(ep string, args map[string]string) *url.URL {
	path := ep
	for k, v := range args {
		path = strings.Replace(path, ":"+k, v, -1)
	}

	return &url.URL{
		Host: "test:9093",
		Path: path,
	}
}

func (c *fakeAPIClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
	test := <-c.ch

	if req.URL.Path != test.path {
		c.Errorf("unexpected request path: want %s, got %s", test.path, req.URL.Path)
	}
	if req.Method != test.method {
		c.Errorf("unexpected request method: want %s, got %s", test.method, req.Method)
	}

	b, err := json.Marshal(test.res)
	if err != nil {
		c.Fatal(err)
	}

	return &http.Response{}, b, test.err
}

func TestAPI(t *testing.T) {
	client := &fakeAPIClient{T: t, ch: make(chan fakeAPIResponse, 1)}
	now := time.Now()

	u, err := url.Parse("http://example.com")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	statusData := &ServerStatus{
		ConfigYAML: "{}",
		ConfigJSON: &config.Config{
			Global: &config.GlobalConfig{
				PagerdutyURL:  &config.URL{URL: u},
				SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"},
			},
		},
		VersionInfo:   map[string]string{"version": "v1"},
		Uptime:        now,
		ClusterStatus: &ClusterStatus{Peers: []PeerStatus{}},
	}
	doStatus := func() (interface{}, error) {
		api := httpStatusAPI{client: client}
		return api.Get(context.Background())
	}

	alertOne := APIV1Alert{
		StartsAt:    now,
		EndsAt:      now.Add(time.Duration(5 * time.Minute)),
		Labels:      LabelSet{"label1": "test1"},
		Annotations: LabelSet{"annotation1": "some text"},
	}
	alerts := []*ExtendedAlert{
		{
			APIV1Alert:  alertOne,
			Fingerprint: "1c93eec3511dc156",
			Status: types.AlertStatus{
				State: types.AlertStateActive,
			},
		},
	}
	doAlertList := func() (interface{}, error) {
		api := httpAlertAPI{client: client}
		return api.List(context.Background(), "", "", false, false, false, false)
	}
	doAlertPush := func() (interface{}, error) {
		api := httpAlertAPI{client: client}
		return nil, api.Push(context.Background(), []APIV1Alert{alertOne}...)
	}

	silOne := &types.Silence{
		ID: "abc",
		Matchers: []*labels.Matcher{
			{
				Name:  "label1",
				Value: "test1",
				Type:  labels.MatchEqual,
			},
		},
		StartsAt:  now,
		EndsAt:    now.Add(time.Duration(2 * time.Hour)),
		UpdatedAt: now,
		CreatedBy: "alice",
		Comment:   "some comment",
		Status: types.SilenceStatus{
			State: "active",
		},
	}
	doSilenceGet := func(id string) func() (interface{}, error) {
		return func() (interface{}, error) {
			api := httpSilenceAPI{client: client}
			return api.Get(context.Background(), id)
		}
	}
	doSilenceSet := func(sil types.Silence) func() (interface{}, error) {
		return func() (interface{}, error) {
			api := httpSilenceAPI{client: client}
			return api.Set(context.Background(), sil)
		}
	}
	doSilenceExpire := func(id string) func() (interface{}, error) {
		return func() (interface{}, error) {
			api := httpSilenceAPI{client: client}
			return nil, api.Expire(context.Background(), id)
		}
	}
	doSilenceList := func() (interface{}, error) {
		api := httpSilenceAPI{client: client}
		return api.List(context.Background(), "")
	}

	tests := []apiTest{
		{
			do: doStatus,
			apiRes: fakeAPIResponse{
				res:    statusData,
				path:   "/api/v1/status",
				method: http.MethodGet,
			},
			res: statusData,
		},
		{
			do: doStatus,
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/status",
				method: http.MethodGet,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doAlertList,
			apiRes: fakeAPIResponse{
				res:    alerts,
				path:   "/api/v1/alerts",
				method: http.MethodGet,
			},
			res: alerts,
		},
		{
			do: doAlertList,
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/alerts",
				method: http.MethodGet,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doAlertPush,
			apiRes: fakeAPIResponse{
				res:    nil,
				path:   "/api/v1/alerts",
				method: http.MethodPost,
			},
			res: nil,
		},
		{
			do: doAlertPush,
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/alerts",
				method: http.MethodPost,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doSilenceGet("abc"),
			apiRes: fakeAPIResponse{
				res:    silOne,
				path:   "/api/v1/silence/abc",
				method: http.MethodGet,
			},
			res: silOne,
		},
		{
			do: doSilenceGet("abc"),
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/silence/abc",
				method: http.MethodGet,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doSilenceSet(*silOne),
			apiRes: fakeAPIResponse{
				res:    map[string]string{"SilenceId": "abc"},
				path:   "/api/v1/silences",
				method: http.MethodPost,
			},
			res: "abc",
		},
		{
			do: doSilenceSet(*silOne),
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/silences",
				method: http.MethodPost,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doSilenceExpire("abc"),
			apiRes: fakeAPIResponse{
				path:   "/api/v1/silence/abc",
				method: http.MethodDelete,
			},
		},
		{
			do: doSilenceExpire("abc"),
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/silence/abc",
				method: http.MethodDelete,
			},
			err: fmt.Errorf("some error"),
		},
		{
			do: doSilenceList,
			apiRes: fakeAPIResponse{
				res:    []*types.Silence{silOne},
				path:   "/api/v1/silences",
				method: http.MethodGet,
			},
			res: []*types.Silence{silOne},
		},
		{
			do: doSilenceList,
			apiRes: fakeAPIResponse{
				err:    fmt.Errorf("some error"),
				path:   "/api/v1/silences",
				method: http.MethodGet,
			},
			err: fmt.Errorf("some error"),
		},
	}
	for _, test := range tests {
		test := test
		client.ch <- test.apiRes
		t.Run(fmt.Sprintf("%s %s", test.apiRes.method, test.apiRes.path), func(t *testing.T) {
			res, err := test.do()
			if test.err != nil {
				if err == nil {
					t.Errorf("unexpected error: want: %s but got none", test.err)
					return
				}
				if err.Error() != test.err.Error() {
					t.Errorf("unexpected error: want: %s, got: %s", test.err, err)
				}
				return
			}
			if err != nil {
				t.Errorf("unexpected error: %s", err)
				return
			}
			want, err := json.Marshal(test.res)
			if err != nil {
				t.Fatal(err)
			}
			got, err := json.Marshal(res)
			if err != nil {
				t.Fatal(err)
			}
			if !bytes.Equal(want, got) {
				t.Errorf("unexpected result: want: %s, got: %s", string(want), string(got))
			}
		})
	}
}

// Fake HTTP client for TestAPIClientDo.
type fakeClient struct {
	*testing.T
	ch chan fakeResponse
}

type fakeResponse struct {
	code int
	res  interface{}
	err  error
}

func (c fakeClient) URL(string, map[string]string) *url.URL {
	return nil
}

func (c fakeClient) Do(context.Context, *http.Request) (*http.Response, []byte, error) {
	fakeRes := <-c.ch

	if fakeRes.err != nil {
		return nil, nil, fakeRes.err
	}

	var b []byte
	var err error
	switch v := fakeRes.res.(type) {
	case string:
		b = []byte(v)
	default:
		b, err = json.Marshal(v)
		if err != nil {
			c.Fatal(err)
		}
	}

	return &http.Response{StatusCode: fakeRes.code}, b, nil
}

type apiClientTest struct {
	response fakeResponse

	expected string
	err      error
}

func TestAPIClientDo(t *testing.T) {
	tests := []apiClientTest{
		{
			response: fakeResponse{
				code: http.StatusOK,
				res: &apiResponse{
					Status: statusSuccess,
					Data:   json.RawMessage(`"test"`),
				},
				err: nil,
			},
			expected: `"test"`,
			err:      nil,
		},
		{
			response: fakeResponse{
				code: http.StatusBadRequest,
				res: &apiResponse{
					Status: statusError,
					Error:  "some error",
				},
				err: nil,
			},
			err: fmt.Errorf("some error (code: 400)"),
		},
		{
			response: fakeResponse{
				code: http.StatusOK,
				res: &apiResponse{
					Status: statusError,
					Error:  "some error",
				},
				err: nil,
			},
			err: fmt.Errorf("inconsistent body for response code (code: 200)"),
		},
		{
			response: fakeResponse{
				code: http.StatusNotFound,
				res:  "not found",
				err:  nil,
			},
			err: fmt.Errorf("not found (code: 404)"),
		},
		{
			response: fakeResponse{
				err: fmt.Errorf("some error"),
			},
			err: fmt.Errorf("some error"),
		},
	}

	fake := fakeClient{T: t, ch: make(chan fakeResponse, 1)}
	client := apiClient{fake}

	for _, test := range tests {
		t.Run("", func(t *testing.T) {
			fake.ch <- test.response

			_, body, err := client.Do(context.Background(), &http.Request{})
			if test.err != nil {
				if err == nil {
					t.Errorf("expected error %q but got none", test.err)
					return
				}
				if test.err.Error() != err.Error() {
					t.Errorf("unexpected error: want %q, got %q", test.err, err)
					return
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error %q", err)
				return
			}

			want, got := test.expected, string(body)
			if want != got {
				t.Errorf("unexpected body: want %q, got %q", want, got)
			}
		})
	}
}
