// Copyright 2016 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package docker

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"reflect"
	"testing"

	"github.com/docker/docker/api/types/swarm"
)

func TestCreateService(t *testing.T) {
	t.Parallel()
	result := `{
  "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
}`
	var expected swarm.Service
	err := json.Unmarshal([]byte(result), &expected)
	if err != nil {
		t.Fatal(err)
	}
	fakeRT := &FakeRoundTripper{message: result, status: http.StatusOK}
	client := newTestClient(fakeRT)
	opts := CreateServiceOptions{}
	service, err := client.CreateService(opts)
	if err != nil {
		t.Fatal(err)
	}
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	if service.ID != id {
		t.Errorf("CreateServce: wrong ID. Want %q. Got %q.", id, service.ID)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodPost {
		t.Errorf("CreateService: wrong HTTP method. Want %q. Got %q.", http.MethodPost, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/create"))
	if gotPath := req.URL.Path; gotPath != expectedURL.Path {
		t.Errorf("CreateService: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath)
	}
	var gotBody Config
	err = json.NewDecoder(req.Body).Decode(&gotBody)
	if err != nil {
		t.Fatal(err)
	}

	authHeader, ok := req.Header["X-Registry-Auth"]
	if ok {
		t.Errorf("CreateService: unexpected non-empty X-Registry-Auth header: %v", authHeader)
	}
}

func TestCreateServiceWithAuthentication(t *testing.T) {
	t.Parallel()
	result := `{
  "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
}`
	var expected swarm.Service
	err := json.Unmarshal([]byte(result), &expected)
	if err != nil {
		t.Fatal(err)
	}
	fakeRT := &FakeRoundTripper{message: result, status: http.StatusOK}
	client := newTestClient(fakeRT)
	opts := CreateServiceOptions{}
	opts.Auth = AuthConfiguration{
		Username: "gopher",
		Password: "gopher123",
		Email:    "gopher@tsuru.io",
	}
	service, err := client.CreateService(opts)
	if err != nil {
		t.Fatal(err)
	}
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	if service.ID != id {
		t.Errorf("CreateServce: wrong ID. Want %q. Got %q.", id, service.ID)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodPost {
		t.Errorf("CreateService: wrong HTTP method. Want %q. Got %q.", http.MethodPost, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/create"))
	if gotPath := req.URL.Path; gotPath != expectedURL.Path {
		t.Errorf("CreateService: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath)
	}
	var gotBody Config
	err = json.NewDecoder(req.Body).Decode(&gotBody)
	if err != nil {
		t.Fatal(err)
	}
	var gotAuth AuthConfiguration

	auth, err := base64.URLEncoding.DecodeString(req.Header.Get("X-Registry-Auth"))
	if err != nil {
		t.Errorf("CreateService: caught error decoding auth. %#v", err.Error())
	}

	err = json.Unmarshal(auth, &gotAuth)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(gotAuth, opts.Auth) {
		t.Errorf("CreateService: wrong auth configuration. Want %#v. Got %#v.", opts.Auth, gotAuth)
	}
}

func TestRemoveService(t *testing.T) {
	t.Parallel()
	fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
	client := newTestClient(fakeRT)
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	opts := RemoveServiceOptions{ID: id}
	err := client.RemoveService(opts)
	if err != nil {
		t.Fatal(err)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodDelete {
		t.Errorf("RemoveService(%q): wrong HTTP method. Want %q. Got %q.", id, http.MethodDelete, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/" + id))
	if gotPath := req.URL.Path; gotPath != expectedURL.Path {
		t.Errorf("RemoveService(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
	}
}

func TestRemoveServiceNotFound(t *testing.T) {
	t.Parallel()
	client := newTestClient(&FakeRoundTripper{message: "no such service", status: http.StatusNotFound})
	err := client.RemoveService(RemoveServiceOptions{ID: "a2334"})
	expected := &NoSuchService{ID: "a2334"}
	if e := err.(*NoSuchService); e.ID != expected.ID {
		t.Errorf("RemoveService: Wrong error returned. Want %#v. Got %#v.", expected, err)
	}
}

func TestUpdateService(t *testing.T) {
	t.Parallel()
	fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
	client := newTestClient(fakeRT)
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	update := UpdateServiceOptions{Version: 23}
	err := client.UpdateService(id, update)
	if err != nil {
		t.Fatal(err)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodPost {
		t.Errorf("UpdateService: wrong HTTP method. Want %q. Got %q.", http.MethodPost, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/" + id + "/update?version=23"))
	if gotURI := req.URL.RequestURI(); gotURI != expectedURL.RequestURI() {
		t.Errorf("UpdateService: Wrong path in request. Want %q. Got %q.", expectedURL.RequestURI(), gotURI)
	}
	expectedContentType := "application/json"
	if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType {
		t.Errorf("UpdateService: Wrong content-type in request. Want %q. Got %q.", expectedContentType, contentType)
	}
	var out UpdateServiceOptions
	if err := json.NewDecoder(req.Body).Decode(&out); err != nil {
		t.Fatal(err)
	}
	update.Version = 0
	if !reflect.DeepEqual(out, update) {
		t.Errorf("UpdateService: wrong body\ngot  %#v\nwant %#v", out, update)
	}
}

func TestUpdateServiceRollback(t *testing.T) {
	t.Parallel()
	fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
	client := newTestClient(fakeRT)
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	update := UpdateServiceOptions{Version: 23, Rollback: "previous"}
	err := client.UpdateService(id, update)
	if err != nil {
		t.Fatal(err)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodPost {
		t.Errorf("UpdateService: wrong HTTP method. Want %q. Got %q.", http.MethodPost, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/" + id + "/update?version=23&rollback=previous"))
	if req.URL.Path != expectedURL.Path {
		t.Errorf("UpdateService: Wrong path in request. Want %q. Got %q.", expectedURL.Path, req.URL.Path)
	}
	if !reflect.DeepEqual(req.URL.Query(), expectedURL.Query()) {
		t.Errorf("UpdateService: Wrong querystring in request. Want %v. Got %v.", expectedURL.Query(), req.URL.Query())
	}
}

func TestUpdateServiceWithAuthentication(t *testing.T) {
	t.Parallel()
	fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
	client := newTestClient(fakeRT)
	id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
	update := UpdateServiceOptions{Version: 23}
	update.Auth = AuthConfiguration{
		Username: "gopher",
		Password: "gopher123",
		Email:    "gopher@tsuru.io",
	}

	err := client.UpdateService(id, update)
	if err != nil {
		t.Fatal(err)
	}
	req := fakeRT.requests[0]
	if req.Method != http.MethodPost {
		t.Errorf("UpdateService: wrong HTTP method. Want %q. Got %q.", http.MethodPost, req.Method)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/" + id + "/update?version=23"))
	if gotURI := req.URL.RequestURI(); gotURI != expectedURL.RequestURI() {
		t.Errorf("UpdateService: Wrong path in request. Want %q. Got %q.", expectedURL.RequestURI(), gotURI)
	}
	expectedContentType := "application/json"
	if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType {
		t.Errorf("UpdateService: Wrong content-type in request. Want %q. Got %q.", expectedContentType, contentType)
	}
	var out UpdateServiceOptions
	if err := json.NewDecoder(req.Body).Decode(&out); err != nil {
		t.Fatal(err)
	}
	var updateAuth AuthConfiguration

	auth, err := base64.URLEncoding.DecodeString(req.Header.Get("X-Registry-Auth"))
	if err != nil {
		t.Errorf("UpdateService: caught error decoding auth. %#v", err.Error())
	}

	err = json.Unmarshal(auth, &updateAuth)
	if err != nil {
		t.Fatal(err)
	}

	if !reflect.DeepEqual(updateAuth, update.Auth) {
		t.Errorf("UpdateService: wrong auth configuration. Want %#v. Got %#v", update.Auth, updateAuth)
	}
}

func TestUpdateServiceNotFound(t *testing.T) {
	t.Parallel()
	client := newTestClient(&FakeRoundTripper{message: "no such service", status: http.StatusNotFound})
	update := UpdateServiceOptions{}
	err := client.UpdateService("notfound", update)
	expected := &NoSuchService{ID: "notfound"}
	if e := err.(*NoSuchService); e.ID != expected.ID {
		t.Errorf("UpdateService: Wrong error returned. Want %#v. Got %#v.", expected, err)
	}
}

func TestInspectServiceNotFound(t *testing.T) {
	t.Parallel()
	client := newTestClient(&FakeRoundTripper{message: "no such service", status: http.StatusNotFound})
	service, err := client.InspectService("notfound")
	if service != nil {
		t.Errorf("InspectService: Expected <nil> service, got %#v", service)
	}
	expected := &NoSuchService{ID: "notfound"}
	if e := err.(*NoSuchService); e.ID != expected.ID {
		t.Errorf("InspectService: Wrong error returned. Want %#v. Got %#v.", expected, err)
	}
}

func TestInspectService(t *testing.T) {
	t.Parallel()
	jsonService := `{
  "ID": "ak7w3gjqoa3kuz8xcpnyy0pvl",
  "Version": {
    "Index": 95
  },
  "CreatedAt": "2016-06-07T21:10:20.269723157Z",
  "UpdatedAt": "2016-06-07T21:10:20.276301259Z",
  "Spec": {
    "Name": "redis",
    "Task": {
      "ContainerSpec": {
        "Image": "redis"
      },
      "Resources": {
        "Limits": {},
        "Reservations": {}
      },
      "RestartPolicy": {
        "Condition": "ANY"
      },
      "Placement": {}
    },
    "Mode": {
      "Replicated": {
        "Replicas": 1
      }
    },
    "UpdateConfig": {
      "Parallelism": 1
    },
    "EndpointSpec": {
      "Mode": "VIP",
      "Ingress": "PUBLICPORT",
      "ExposedPorts": [
        {
          "Protocol": "tcp",
          "Port": 6379
        }
      ]
    }
  },
  "Endpoint": {
    "Spec": {},
    "ExposedPorts": [
      {
        "Protocol": "tcp",
        "Port": 6379,
        "PublicPort": 30001
      }
    ],
    "VirtualIPs": [
      {
        "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
        "Addr": "10.255.0.4/16"
      }
    ]
  }
}`
	var expected swarm.Service
	err := json.Unmarshal([]byte(jsonService), &expected)
	if err != nil {
		t.Fatal(err)
	}
	fakeRT := &FakeRoundTripper{message: jsonService, status: http.StatusOK}
	client := newTestClient(fakeRT)
	id := "ak7w3gjqoa3kuz8xcpnyy0pvl"
	service, err := client.InspectService(id)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(*service, expected) {
		t.Errorf("InspectService(%q): Expected %#v. Got %#v.", id, expected, service)
	}
	expectedURL, _ := url.Parse(client.getURL("/services/ak7w3gjqoa3kuz8xcpnyy0pvl"))
	if gotPath := fakeRT.requests[0].URL.Path; gotPath != expectedURL.Path {
		t.Errorf("InspectService(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
	}
}

func TestListServices(t *testing.T) {
	t.Parallel()
	jsonServices := `[
  {
    "ID": "9mnpnzenvg8p8tdbtq4wvbkcz",
    "Version": {
      "Index": 19
    },
    "CreatedAt": "2016-06-07T21:05:51.880065305Z",
    "UpdatedAt": "2016-06-07T21:07:29.962229872Z",
    "Spec": {
      "Name": "hopeful_cori",
      "TaskTemplate": {
        "ContainerSpec": {
          "Image": "redis"
        },
        "Resources": {
          "Limits": {},
          "Reservations": {}
        },
        "RestartPolicy": {
          "Condition": "ANY"
        },
        "Placement": {}
      },
      "Mode": {
        "Replicated": {
          "Replicas": 1
        }
      },
      "UpdateConfig": {
        "Parallelism": 1
      },
      "EndpointSpec": {
        "Mode": "VIP",
        "Ingress": "PUBLICPORT",
        "ExposedPorts": [
          {
            "Protocol": "tcp",
            "Port": 6379
          }
        ]
      }
    },
    "Endpoint": {
      "Spec": {},
      "ExposedPorts": [
        {
          "Protocol": "tcp",
          "Port": 6379,
          "PublicPort": 30000
        }
      ],
      "VirtualIPs": [
        {
          "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
          "Addr": "10.255.0.2/16"
        },
        {
          "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
          "Addr": "10.255.0.3/16"
        }
      ]
    }
  }
]`
	var expected []swarm.Service
	err := json.Unmarshal([]byte(jsonServices), &expected)
	if err != nil {
		t.Fatal(err)
	}
	client := newTestClient(&FakeRoundTripper{message: jsonServices, status: http.StatusOK})
	services, err := client.ListServices(ListServicesOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(services, expected) {
		t.Errorf("ListServices: Expected %#v. Got %#v.", expected, services)
	}
}

/// ##################################################""

func TestGetServiceLogs(t *testing.T) {
	var req http.Request
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
		w.Write(prefix)
		w.Write([]byte("something happened!"))
		req = *r
	}))
	defer server.Close()
	client, _ := NewClient(server.URL)
	client.SkipServerVersionCheck = true
	var buf bytes.Buffer
	opts := LogsServiceOptions{
		Service:      "a123456",
		OutputStream: &buf,
		Follow:       true,
		Stdout:       true,
		Stderr:       true,
		Timestamps:   true,
	}
	err := client.GetServiceLogs(opts)
	if err != nil {
		t.Fatal(err)
	}
	expected := "something happened!"
	if buf.String() != expected {
		t.Errorf("Logs: wrong output. Want %q. Got %q.", expected, buf.String())
	}
	if req.Method != http.MethodGet {
		t.Errorf("Logs: wrong HTTP method. Want GET. Got %s.", req.Method)
	}
	u, _ := url.Parse(client.getURL("/services/a123456/logs"))
	if req.URL.Path != u.Path {
		t.Errorf("AttachToContainer for logs: wrong HTTP path. Want %q. Got %q.", u.Path, req.URL.Path)
	}
	expectedQs := map[string][]string{
		"follow":     {"1"},
		"stdout":     {"1"},
		"stderr":     {"1"},
		"timestamps": {"1"},
		"tail":       {"all"},
	}
	got := map[string][]string(req.URL.Query())
	if !reflect.DeepEqual(got, expectedQs) {
		t.Errorf("Logs: wrong query string. Want %#v. Got %#v.", expectedQs, got)
	}
}

func TestGetServicetLogsNilStdoutDoesntFail(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
		w.Write(prefix)
		w.Write([]byte("something happened!"))
	}))
	defer server.Close()
	client, _ := NewClient(server.URL)
	client.SkipServerVersionCheck = true
	opts := LogsServiceOptions{
		Service:    "a123456",
		Follow:     true,
		Stdout:     true,
		Stderr:     true,
		Timestamps: true,
	}
	err := client.GetServiceLogs(opts)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGetServiceLogsNilStderrDoesntFail(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		prefix := []byte{2, 0, 0, 0, 0, 0, 0, 19}
		w.Write(prefix)
		w.Write([]byte("something happened!"))
	}))
	defer server.Close()
	client, _ := NewClient(server.URL)
	client.SkipServerVersionCheck = true
	opts := LogsServiceOptions{
		Service:    "a123456",
		Follow:     true,
		Stdout:     true,
		Stderr:     true,
		Timestamps: true,
	}
	err := client.GetServiceLogs(opts)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGetServiceLogsSpecifyingTail(t *testing.T) {
	var req http.Request
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
		w.Write(prefix)
		w.Write([]byte("something happened!"))
		req = *r
	}))
	defer server.Close()
	client, _ := NewClient(server.URL)
	client.SkipServerVersionCheck = true
	var buf bytes.Buffer
	opts := LogsServiceOptions{
		Service:      "a123456",
		OutputStream: &buf,
		Follow:       true,
		Stdout:       true,
		Stderr:       true,
		Timestamps:   true,
		Tail:         "100",
	}
	err := client.GetServiceLogs(opts)
	if err != nil {
		t.Fatal(err)
	}
	expected := "something happened!"
	if buf.String() != expected {
		t.Errorf("Logs: wrong output. Want %q. Got %q.", expected, buf.String())
	}
	if req.Method != http.MethodGet {
		t.Errorf("Logs: wrong HTTP method. Want GET. Got %s.", req.Method)
	}
	u, _ := url.Parse(client.getURL("/services/a123456/logs"))
	if req.URL.Path != u.Path {
		t.Errorf("AttachToContainer for logs: wrong HTTP path. Want %q. Got %q.", u.Path, req.URL.Path)
	}
	expectedQs := map[string][]string{
		"follow":     {"1"},
		"stdout":     {"1"},
		"stderr":     {"1"},
		"timestamps": {"1"},
		"tail":       {"100"},
	}
	got := map[string][]string(req.URL.Query())
	if !reflect.DeepEqual(got, expectedQs) {
		t.Errorf("Logs: wrong query string. Want %#v. Got %#v.", expectedQs, got)
	}
}

func TestGetServiceLogsRawTerminal(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Write([]byte("something happened!"))
	}))
	defer server.Close()
	client, _ := NewClient(server.URL)
	client.SkipServerVersionCheck = true
	var buf bytes.Buffer
	opts := LogsServiceOptions{
		Service:      "a123456",
		OutputStream: &buf,
		Follow:       true,
		RawTerminal:  true,
		Stdout:       true,
		Stderr:       true,
		Timestamps:   true,
		Tail:         "100",
	}
	err := client.GetServiceLogs(opts)
	if err != nil {
		t.Fatal(err)
	}
	expected := "something happened!"
	if buf.String() != expected {
		t.Errorf("Logs: wrong output. Want %q. Got %q.", expected, buf.String())
	}
}

func TestGetServiceLogsNoContainer(t *testing.T) {
	var client Client
	err := client.GetServiceLogs(LogsServiceOptions{})
	expected := &NoSuchService{ID: ""}
	if e := err.(*NoSuchService); e.ID != expected.ID {
		t.Errorf("AttachToContainer: wrong error. Want %#v. Got %#v.", expected, err)
	}
}
