/*
Copyright 2019 The Knative 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 prober

import (
	"context"
	"errors"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/util/wait"
	"knative.dev/pkg/network"
)

const (
	systemName             = "test-server"
	unexpectedProbeMessage = "unexpected probe header value: whatever"
	probeInterval          = 10 * time.Millisecond
	probeTimeout           = 200 * time.Millisecond
)

func probeServeFunc(w http.ResponseWriter, r *http.Request) {
	s := r.Header.Get(network.ProbeHeaderName)
	switch s {
	case "":
		// No header.
		w.WriteHeader(http.StatusNotFound)
	case systemName:
		// Expected header value.
		w.Write([]byte(systemName))
	default:
		// Unexpected header value.
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(unexpectedProbeMessage))
	}
}

func TestDoServing(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
	defer ts.Close()
	tests := []struct {
		name        string
		headerValue string
		want        bool
		expErr      bool
	}{{
		name:        "ok",
		headerValue: systemName,
		want:        true,
		expErr:      false,
	}, {
		name:        "wrong system",
		headerValue: "bells-and-whistles",
		want:        false,
		expErr:      true,
	}, {
		name:        "no header",
		headerValue: "",
		want:        false,
		expErr:      true,
	}}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, err := Do(context.Background(), network.NewProberTransport(), ts.URL, WithHeader(network.ProbeHeaderName, test.headerValue), ExpectsBody(systemName), ExpectsStatusCodes([]int{http.StatusOK}))
			if want := test.want; got != want {
				t.Errorf("Got = %v, want: %v", got, want)
			}
			if err != nil && !test.expErr {
				t.Errorf("Do() = %v, no error expected", err)
			}
			if err == nil && test.expErr {
				t.Errorf("Do() = nil, expected an error")
			}
		})
	}
}

func TestBlackHole(t *testing.T) {
	transport := &http.Transport{
		Dial: (&net.Dialer{
			Timeout: 10 * time.Millisecond,
		}).Dial,
	}
	got, err := Do(context.Background(), transport, "http://gone.fishing.svc.custer.local:8080", ExpectsStatusCodes([]int{http.StatusOK}))
	if want := false; got != want {
		t.Errorf("Got = %v, want: %v", got, want)
	}
	if err == nil {
		t.Error("Do did not return an error")
	}
}

func TestBadURL(t *testing.T) {
	_, err := Do(context.Background(), network.NewProberTransport(), ":foo", ExpectsStatusCodes([]int{http.StatusOK}))
	if err == nil {
		t.Error("Do did not return an error")
	}
	t.Log("For the curious the error was:", err)
}

func TestDoAsync(t *testing.T) {
	// This replicates the TestDo.
	ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
	defer ts.Close()

	wch := make(chan interface{})
	defer close(wch)
	tests := []struct {
		name        string
		headerValue string
		cb          Done
	}{{
		name:        "ok",
		headerValue: systemName,
		cb: func(arg interface{}, ret bool, err error) {
			defer func() {
				wch <- 42
			}()
			if got, want := arg.(string), "ok"; got != want {
				t.Errorf("arg = %s, want: %s", got, want)
			}
			if !ret {
				t.Error("result was false")
			}
		},
	}, {
		name:        "wrong system",
		headerValue: "bells-and-whistles",
		cb: func(arg interface{}, ret bool, err error) {
			defer func() {
				wch <- 1984
			}()
			if ret {
				t.Error("result was true")
			}
		},
	}, {
		name:        "no header",
		headerValue: "",
		cb: func(arg interface{}, ret bool, err error) {
			defer func() {
				wch <- 2006
			}()
			if ret {
				t.Error("result was true")
			}
		},
	}}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			m := New(test.cb, network.NewProberTransport())
			m.Offer(context.Background(), ts.URL, test.name, probeInterval, probeTimeout, WithHeader(network.ProbeHeaderName, test.headerValue), ExpectsBody(test.headerValue), ExpectsStatusCodes([]int{http.StatusOK}))
			<-wch
		})
	}
}

type thirdTimesTheCharmProber struct {
	calls int
}

func (t *thirdTimesTheCharmProber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	t.calls++
	if t.calls < 3 {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(unexpectedProbeMessage))
		return
	}
	w.Write([]byte(systemName))
}

func TestDoAsyncRepeat(t *testing.T) {
	c := &thirdTimesTheCharmProber{}
	ts := httptest.NewServer(c)
	defer ts.Close()

	wch := make(chan interface{})
	defer close(wch)
	cb := func(arg interface{}, done bool, err error) {
		if !done {
			t.Error("done was false")
		}
		if err != nil {
			t.Error("Unexpected error =", err)
		}
		wch <- arg
	}
	m := New(cb, network.NewProberTransport())
	m.Offer(context.Background(), ts.URL, 42, probeInterval, probeTimeout, WithHeader(network.ProbeHeaderName, systemName), ExpectsBody(systemName), ExpectsStatusCodes([]int{http.StatusOK}))
	<-wch
	if got, want := c.calls, 3; got != want {
		t.Errorf("Probe invocation count = %d, want: %d", got, want)
	}
}

func TestDoAsyncTimeout(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer ts.Close()

	wch := make(chan interface{})
	defer close(wch)

	cb := func(arg interface{}, done bool, err error) {
		if done {
			t.Errorf("done was true")
		}
		if !errors.Is(err, wait.ErrWaitTimeout) {
			t.Error("Unexpected error =", err)
		}
		wch <- arg
	}
	m := New(cb, network.NewProberTransport())
	m.Offer(context.Background(), ts.URL, 2009, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK}))
	<-wch
}

func TestAsyncMultiple(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
	defer ts.Close()

	wch := make(chan interface{})
	defer close(wch)
	cb := func(arg interface{}, done bool, err error) {
		<-wch
		wch <- 2006
	}
	m := New(cb, network.NewProberTransport())
	if !m.Offer(context.Background(), ts.URL, 1984, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK})) {
		t.Error("First call to offer returned false")
	}
	if m.Offer(context.Background(), ts.URL, 1982, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK})) {
		t.Error("Second call to offer returned true")
	}
	if got, want := m.len(), 1; got != want {
		t.Errorf("Number of queued items = %d, want: %d", got, want)
	}
	// Make sure we terminate the first probe.
	wch <- 2009
	<-wch

	wait.PollImmediate(probeInterval, probeTimeout, func() (bool, error) {
		return m.len() == 0, nil
	})
	if got, want := m.len(), 0; got != want {
		t.Errorf("Number of queued items = %d, want: %d", got, want)
	}
}

func TestWithPathOption(t *testing.T) {
	const path = "/correct/probe/path/"
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Logf("Got: %s, Want: %s\n", r.URL.Path, path)
		if r.URL.Path != path {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer ts.Close()

	tests := []struct {
		name    string
		options []interface{}
	}{{
		name:    "no path",
		options: []interface{}{ExpectsStatusCodes([]int{http.StatusNotFound})},
	}, {
		name:    "expected path",
		options: []interface{}{WithPath(path), ExpectsStatusCodes([]int{http.StatusOK})},
	}, {
		name:    "wrong path",
		options: []interface{}{WithPath("/wrong/path"), ExpectsStatusCodes([]int{http.StatusNotFound})},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if ok, _ := Do(context.Background(), network.AutoTransport, ts.URL, test.options...); !ok {
				t.Error("Unexpected probe failure")
			}
		})
	}
}

func TestWithHostOption(t *testing.T) {
	host := "foobar.com"
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Logf("Want: %s, Got: %s\n", host, r.Host)
		if r.Host != host {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer ts.Close()

	tests := []struct {
		name    string
		options []interface{}
	}{{
		name:    "no hosts",
		options: []interface{}{ExpectsStatusCodes([]int{http.StatusNotFound})},
	}, {
		name:    "expected host",
		options: []interface{}{WithHost(host), ExpectsStatusCodes([]int{http.StatusOK})},
	}, {
		name:    "wrong host",
		options: []interface{}{WithHost("nope.com"), ExpectsStatusCodes([]int{http.StatusNotFound})},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if ok, _ := Do(context.Background(), network.AutoTransport, ts.URL, test.options...); !ok {
				t.Error("Unexpected probe result")
			}
		})
	}
}

func TestExpectsHeaderOption(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Foo", "Bar")
	}))
	defer ts.Close()

	tests := []struct {
		name    string
		options []interface{}
		success bool
		expErr  bool
	}{{
		name:    "header is present",
		options: []interface{}{ExpectsHeader("Foo", "Bar"), ExpectsStatusCodes([]int{http.StatusOK})},
		success: true,
	}, {
		name:    "header is absent",
		options: []interface{}{ExpectsHeader("Baz", "Nope"), ExpectsStatusCodes([]int{http.StatusOK})},
		success: false,
		expErr:  true,
	}, {
		name:    "header value doesn't match",
		options: []interface{}{ExpectsHeader("Foo", "Baz"), ExpectsStatusCodes([]int{http.StatusOK})},
		success: false,
		expErr:  true,
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ok, err := Do(context.Background(), network.AutoTransport, ts.URL, test.options...)
			if ok != test.success {
				t.Errorf("unexpected probe result: want: %v, got: %v", test.success, ok)
			}
			if err != nil && !test.expErr {
				t.Errorf("Do() = %v, no error expected", err)
			}
			if err == nil && test.expErr {
				t.Errorf("Do() = nil, expected an error")
			}
		})
	}
}

func (m *Manager) len() int {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.keys.Len()
}
