package credentials

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"strings"
	"testing"

	"github.com/docker/docker-credential-helpers/client"
	"github.com/docker/docker-credential-helpers/credentials"
	"github.com/docker/docker/api/types"
)

const (
	validServerAddress   = "https://index.docker.io/v1"
	validServerAddress2  = "https://example.com:5002"
	invalidServerAddress = "https://foobar.example.com"
	missingCredsAddress  = "https://missing.docker.io/v1"
)

var errCommandExited = fmt.Errorf("exited 1")

// mockCommand simulates interactions between the docker client and a remote
// credentials helper.
// Unit tests inject this mocked command into the remote to control execution.
type mockCommand struct {
	arg   string
	input io.Reader
}

// Output returns responses from the remote credentials helper.
// It mocks those responses based in the input in the mock.
func (m *mockCommand) Output() ([]byte, error) {
	in, err := ioutil.ReadAll(m.input)
	if err != nil {
		return nil, err
	}
	inS := string(in)

	switch m.arg {
	case "erase":
		switch inS {
		case validServerAddress:
			return nil, nil
		default:
			return []byte("program failed"), errCommandExited
		}
	case "get":
		switch inS {
		case validServerAddress:
			return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
		case validServerAddress2:
			return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
		case missingCredsAddress:
			return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited
		case invalidServerAddress:
			return []byte("program failed"), errCommandExited
		}
	case "store":
		var c credentials.Credentials
		err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
		if err != nil {
			return []byte("program failed"), errCommandExited
		}
		switch c.ServerURL {
		case validServerAddress:
			return nil, nil
		default:
			return []byte("program failed"), errCommandExited
		}
	case "list":
		return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>")), nil
	}

	return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited
}

// Input sets the input to send to a remote credentials helper.
func (m *mockCommand) Input(in io.Reader) {
	m.input = in
}

func mockCommandFn(args ...string) client.Program {
	return &mockCommand{
		arg: args[0],
	}
}

func TestNativeStoreAddCredentials(t *testing.T) {
	f := newConfigFile(make(map[string]types.AuthConfig))
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	err := s.Store(types.AuthConfig{
		Username:      "foo",
		Password:      "bar",
		Email:         "foo@example.com",
		ServerAddress: validServerAddress,
	})

	if err != nil {
		t.Fatal(err)
	}

	if len(f.AuthConfigs) != 1 {
		t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs))
	}

	a, ok := f.AuthConfigs[validServerAddress]
	if !ok {
		t.Fatalf("expected auth for %s, got %v", validServerAddress, f.AuthConfigs)
	}
	if a.Auth != "" {
		t.Fatalf("expected auth to be empty, got %s", a.Auth)
	}
	if a.Username != "" {
		t.Fatalf("expected username to be empty, got %s", a.Username)
	}
	if a.Password != "" {
		t.Fatalf("expected password to be empty, got %s", a.Password)
	}
	if a.IdentityToken != "" {
		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
	}
	if a.Email != "foo@example.com" {
		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
	}
}

func TestNativeStoreAddInvalidCredentials(t *testing.T) {
	f := newConfigFile(make(map[string]types.AuthConfig))
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	err := s.Store(types.AuthConfig{
		Username:      "foo",
		Password:      "bar",
		Email:         "foo@example.com",
		ServerAddress: invalidServerAddress,
	})

	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if !strings.Contains(err.Error(), "program failed") {
		t.Fatalf("expected `program failed`, got %v", err)
	}

	if len(f.AuthConfigs) != 0 {
		t.Fatalf("expected 0 auth config, got %d", len(f.AuthConfigs))
	}
}

func TestNativeStoreGet(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	a, err := s.Get(validServerAddress)
	if err != nil {
		t.Fatal(err)
	}

	if a.Username != "foo" {
		t.Fatalf("expected username `foo`, got %s", a.Username)
	}
	if a.Password != "bar" {
		t.Fatalf("expected password `bar`, got %s", a.Password)
	}
	if a.IdentityToken != "" {
		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
	}
	if a.Email != "foo@example.com" {
		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
	}
}

func TestNativeStoreGetIdentityToken(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress2: {
			Email: "foo@example2.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	a, err := s.Get(validServerAddress2)
	if err != nil {
		t.Fatal(err)
	}

	if a.Username != "" {
		t.Fatalf("expected username to be empty, got %s", a.Username)
	}
	if a.Password != "" {
		t.Fatalf("expected password to be empty, got %s", a.Password)
	}
	if a.IdentityToken != "abcd1234" {
		t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
	}
	if a.Email != "foo@example2.com" {
		t.Fatalf("expected email `foo@example2.com`, got %s", a.Email)
	}
}

func TestNativeStoreGetAll(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	as, err := s.GetAll()
	if err != nil {
		t.Fatal(err)
	}

	if len(as) != 2 {
		t.Fatalf("wanted 2, got %d", len(as))
	}

	if as[validServerAddress].Username != "foo" {
		t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username)
	}
	if as[validServerAddress].Password != "bar" {
		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
	}
	if as[validServerAddress].IdentityToken != "" {
		t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
	}
	if as[validServerAddress].Email != "foo@example.com" {
		t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
	}
	if as[validServerAddress2].Username != "" {
		t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
	}
	if as[validServerAddress2].Password != "" {
		t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
	}
	if as[validServerAddress2].IdentityToken != "abcd1234" {
		t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
	}
	if as[validServerAddress2].Email != "" {
		t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email)
	}
}

func TestNativeStoreGetMissingCredentials(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	_, err := s.Get(missingCredsAddress)
	if err != nil {
		// missing credentials do not produce an error
		t.Fatal(err)
	}
}

func TestNativeStoreGetInvalidAddress(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	_, err := s.Get(invalidServerAddress)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if !strings.Contains(err.Error(), "program failed") {
		t.Fatalf("expected `program failed`, got %v", err)
	}
}

func TestNativeStoreErase(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	err := s.Erase(validServerAddress)
	if err != nil {
		t.Fatal(err)
	}

	if len(f.AuthConfigs) != 0 {
		t.Fatalf("expected 0 auth configs, got %d", len(f.AuthConfigs))
	}
}

func TestNativeStoreEraseInvalidAddress(t *testing.T) {
	f := newConfigFile(map[string]types.AuthConfig{
		validServerAddress: {
			Email: "foo@example.com",
		},
	})
	f.CredentialsStore = "mock"

	s := &nativeStore{
		programFunc: mockCommandFn,
		fileStore:   NewFileStore(f),
	}
	err := s.Erase(invalidServerAddress)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if !strings.Contains(err.Error(), "program failed") {
		t.Fatalf("expected `program failed`, got %v", err)
	}
}
