// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build go1.13
// +build linux darwin

package main

import (
	"context"
	"fmt"
	"log"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"golang.org/x/build/buildlet"
	"golang.org/x/build/dashboard"
)

type TestBuildletPool struct {
	clients map[string]*buildlet.Client
	mu      sync.Mutex
}

// GetBuildlet finds the first available buildlet for the hostType and returns
// it, or an error if no buildlets are available for that hostType.
func (tp *TestBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg logger) (*buildlet.Client, error) {
	tp.mu.Lock()
	defer tp.mu.Unlock()
	c, ok := tp.clients[hostType]
	if ok {
		return c, nil
	}
	return nil, fmt.Errorf("No client found for host type %s", hostType)
}

// Add sets the given client for the given hostType, overriding any previous
// entries.
func (tp *TestBuildletPool) Add(hostType string, client *buildlet.Client) {
	tp.mu.Lock()
	if tp.clients == nil {
		tp.clients = make(map[string]*buildlet.Client)
	}
	tp.clients[hostType] = client
	tp.mu.Unlock()
}

func (tp *TestBuildletPool) Remove(hostType string) {
	tp.mu.Lock()
	delete(tp.clients, hostType)
	tp.mu.Unlock()
}

func (tp *TestBuildletPool) String() string { return "test" }

func (tp *TestBuildletPool) HasCapacity(string) bool { return true }

var testPool = &TestBuildletPool{}

func TestHandleBuildletCreateWrongMethod(t *testing.T) {
	req := httptest.NewRequest("GET", "/buildlet/create", nil)
	w := httptest.NewRecorder()
	handleBuildletCreate(w, req)
	if w.Code != 400 {
		t.Fatalf("GET /buildlet/create: expected code 400, got %d", w.Code)
	}
	if body := w.Body.String(); !strings.Contains(body, "POST required") {
		t.Fatalf("GET /buildlet/create: expected 'POST required' error, got %s", body)
	}
}

func TestHandleBuildletCreateOldVersion(t *testing.T) {
	data := url.Values{}
	data.Set("version", "20150922")
	req := httptest.NewRequest("POST", "/buildlet/create", strings.NewReader(data.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w := httptest.NewRecorder()
	handleBuildletCreate(w, req)
	if w.Code != 400 {
		t.Fatalf("GET /buildlet/create: expected code 400, got %d", w.Code)
	}
	if body := w.Body.String(); !strings.Contains(body, `client version "20150922" is too old`) {
		t.Fatalf("GET /buildlet/create: expected 'version too old' error, got %s", body)
	}
}

func addBuilder(name string) {
	dashboard.Builders[name] = &dashboard.BuildConfig{
		Name:     name,
		HostType: "test-host",
		Notes:    "Dummy client for testing",
	}
	dashboard.Hosts["test-host"] = &dashboard.HostConfig{
		HostType: "test-host",
		Owner:    "test@golang.org",
	}
	testPool.Add("test-host", &buildlet.Client{})
}

func removeBuilder(name string) {
	delete(dashboard.Builders, name)
	delete(dashboard.Builders, "test-host")
	testPool.Remove("test-host")
}

const buildName = "linux-amd64-test"

type tlogger struct{ t *testing.T }

func (t tlogger) Write(p []byte) (int, error) {
	t.t.Logf("LOG: %s", p)
	return len(p), nil
}

func TestHandleBuildletCreate_PreStream(t *testing.T) {
	log.SetOutput(tlogger{t})
	defer log.SetOutput(os.Stderr)
	addBuilder(buildName)
	remoteBuildlets.m = map[string]*remoteBuildlet{}
	testPoolHook = func(_ *dashboard.HostConfig) BuildletPool { return testPool }
	defer func() {
		timeNow = time.Now
		removeBuilder(buildName)
		testPoolHook = nil
	}()
	timeNow = func() time.Time { return time.Unix(123, 0).In(time.UTC) }
	data := url.Values{}
	data.Set("version", "20160922")
	data.Set("builderType", buildName)
	req := httptest.NewRequest("POST", "/buildlet/create", strings.NewReader(data.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth("gopher", "fake-not-checked-password") // auth is handled in outer wrapper
	w := httptest.NewRecorder()
	handleBuildletCreate(w, req)
	if w.Code != 200 {
		t.Fatal("bad code", w.Code, w.Body.String())
	}
	want := `{"User":"gopher","Name":"gopher-linux-amd64-test-0","HostType":"test-host","BuilderType":"linux-amd64-test","Created":"1970-01-01T00:02:03Z","Expires":"1970-01-01T00:32:03Z"}`
	if got := strings.TrimSpace(w.Body.String()); got != want {
		t.Errorf("unexpected output.\n got: %s\nwant: %s\n", got, want)
	}
}

func TestHandleBuildletCreate_Stream(t *testing.T) {
	log.SetOutput(tlogger{t})
	defer log.SetOutput(os.Stderr)
	addBuilder(buildName)
	remoteBuildlets.m = map[string]*remoteBuildlet{}
	testPoolHook = func(_ *dashboard.HostConfig) BuildletPool { return testPool }
	defer func() {
		timeNow = time.Now
		removeBuilder(buildName)
		testPoolHook = nil
	}()
	timeNow = func() time.Time { return time.Unix(123, 0).In(time.UTC) }
	data := url.Values{}
	data.Set("version", buildlet.GomoteCreateStreamVersion)
	data.Set("builderType", buildName)
	req := httptest.NewRequest("POST", "/buildlet/create", strings.NewReader(data.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth("gopher", "fake-not-checked-password") // auth is handled in outer wrapper
	w := httptest.NewRecorder()
	handleBuildletCreate(w, req)
	if w.Code != 200 {
		t.Fatal("bad code", w.Code, w.Body.String())
	}
	want := `{"buildlet":{"User":"gopher","Name":"gopher-linux-amd64-test-0","HostType":"test-host","BuilderType":"linux-amd64-test","Created":"1970-01-01T00:02:03Z","Expires":"1970-01-01T00:32:03Z"}}`
	if got := strings.TrimSpace(w.Body.String()); got != want {
		t.Errorf("unexpected output.\n got: %s\nwant: %s\n", got, want)
	}
}
