package userpass

import (
	"context"
	"crypto/tls"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/go-test/deep"
	sockaddr "github.com/hashicorp/go-sockaddr"
	logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
	"github.com/hashicorp/vault/sdk/helper/policyutil"
	"github.com/hashicorp/vault/sdk/helper/tokenutil"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/mitchellh/mapstructure"
)

const (
	testSysTTL    = time.Hour * 10
	testSysMaxTTL = time.Hour * 20
)

func TestBackend_CRUD(t *testing.T) {
	var resp *logical.Response
	var err error

	storage := &logical.InmemStorage{}

	config := logical.TestBackendConfig()
	config.StorageView = storage

	ctx := context.Background()

	b, err := Factory(ctx, config)
	if err != nil {
		t.Fatal(err)
	}
	if b == nil {
		t.Fatalf("failed to create backend")
	}

	localhostSockAddr, err := sockaddr.NewSockAddr("127.0.0.1")
	if err != nil {
		t.Fatal(err)
	}

	// Use new token_ forms
	resp, err = b.HandleRequest(ctx, &logical.Request{
		Path:      "users/testuser",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"password":          "testpassword",
			"token_ttl":         5,
			"token_max_ttl":     10,
			"token_policies":    []string{"foo"},
			"token_bound_cidrs": []string{"127.0.0.1"},
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}

	resp, err = b.HandleRequest(ctx, &logical.Request{
		Path:      "users/testuser",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	if resp.Data["token_ttl"].(int64) != 5 && resp.Data["token_max_ttl"].(int64) != 10 {
		t.Fatalf("bad: token_ttl and token_max_ttl are not set correctly")
	}
	if diff := deep.Equal(resp.Data["token_policies"], []string{"foo"}); diff != nil {
		t.Fatal(diff)
	}
	if diff := deep.Equal(resp.Data["token_bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil {
		t.Fatal(diff)
	}

	localhostSockAddr, err = sockaddr.NewSockAddr("127.0.1.1")
	if err != nil {
		t.Fatal(err)
	}

	// Use the old forms and verify that they zero out the new ones and then
	// the new ones read with the expected value
	resp, err = b.HandleRequest(ctx, &logical.Request{
		Path:      "users/testuser",
		Operation: logical.UpdateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"ttl":         "5m",
			"max_ttl":     "10m",
			"policies":    []string{"bar"},
			"bound_cidrs": []string{"127.0.1.1"},
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}

	resp, err = b.HandleRequest(ctx, &logical.Request{
		Path:      "users/testuser",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	if resp.Data["ttl"].(int64) != 300 && resp.Data["max_ttl"].(int64) != 600 {
		t.Fatalf("bad: ttl and max_ttl are not set correctly")
	}
	if resp.Data["token_ttl"].(int64) != 300 && resp.Data["token_max_ttl"].(int64) != 600 {
		t.Fatalf("bad: token_ttl and token_max_ttl are not set correctly")
	}
	if diff := deep.Equal(resp.Data["policies"], []string{"bar"}); diff != nil {
		t.Fatal(diff)
	}
	if diff := deep.Equal(resp.Data["token_policies"], []string{"bar"}); diff != nil {
		t.Fatal(diff)
	}
	if diff := deep.Equal(resp.Data["bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil {
		t.Fatal(diff)
	}
	if diff := deep.Equal(resp.Data["token_bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil {
		t.Fatal(diff)
	}
}

func TestBackend_basic(t *testing.T) {
	b, err := Factory(context.Background(), &logical.BackendConfig{
		Logger: nil,
		System: &logical.StaticSystemView{
			DefaultLeaseTTLVal: testSysTTL,
			MaxLeaseTTLVal:     testSysMaxTTL,
		},
	})
	if err != nil {
		t.Fatalf("Unable to create backend: %s", err)
	}
	logicaltest.Test(t, logicaltest.TestCase{
		CredentialBackend: b,
		Steps: []logicaltest.TestStep{
			testAccStepUser(t, "web", "password", "foo"),
			testAccStepUser(t, "web2", "password", "foo"),
			testAccStepUser(t, "web3", "password", "foo"),
			testAccStepList(t, []string{"web", "web2", "web3"}),
			testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
		},
	})
}

func TestBackend_userCrud(t *testing.T) {
	b, err := Factory(context.Background(), &logical.BackendConfig{
		Logger: nil,
		System: &logical.StaticSystemView{
			DefaultLeaseTTLVal: testSysTTL,
			MaxLeaseTTLVal:     testSysMaxTTL,
		},
	})
	if err != nil {
		t.Fatalf("Unable to create backend: %s", err)
	}

	logicaltest.Test(t, logicaltest.TestCase{
		CredentialBackend: b,
		Steps: []logicaltest.TestStep{
			testAccStepUser(t, "web", "password", "foo"),
			testAccStepReadUser(t, "web", "foo"),
			testAccStepDeleteUser(t, "web"),
			testAccStepReadUser(t, "web", ""),
		},
	})
}

func TestBackend_userCreateOperation(t *testing.T) {
	b, err := Factory(context.Background(), &logical.BackendConfig{
		Logger: nil,
		System: &logical.StaticSystemView{
			DefaultLeaseTTLVal: testSysTTL,
			MaxLeaseTTLVal:     testSysMaxTTL,
		},
	})
	if err != nil {
		t.Fatalf("Unable to create backend: %s", err)
	}

	logicaltest.Test(t, logicaltest.TestCase{
		CredentialBackend: b,
		Steps: []logicaltest.TestStep{
			testUserCreateOperation(t, "web", "password", "foo"),
			testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
		},
	})
}

func TestBackend_passwordUpdate(t *testing.T) {
	b, err := Factory(context.Background(), &logical.BackendConfig{
		Logger: nil,
		System: &logical.StaticSystemView{
			DefaultLeaseTTLVal: testSysTTL,
			MaxLeaseTTLVal:     testSysMaxTTL,
		},
	})
	if err != nil {
		t.Fatalf("Unable to create backend: %s", err)
	}

	logicaltest.Test(t, logicaltest.TestCase{
		CredentialBackend: b,
		Steps: []logicaltest.TestStep{
			testAccStepUser(t, "web", "password", "foo"),
			testAccStepReadUser(t, "web", "foo"),
			testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
			testUpdatePassword(t, "web", "newpassword"),
			testAccStepLogin(t, "web", "newpassword", []string{"default", "foo"}),
		},
	})
}

func TestBackend_policiesUpdate(t *testing.T) {
	b, err := Factory(context.Background(), &logical.BackendConfig{
		Logger: nil,
		System: &logical.StaticSystemView{
			DefaultLeaseTTLVal: testSysTTL,
			MaxLeaseTTLVal:     testSysMaxTTL,
		},
	})
	if err != nil {
		t.Fatalf("Unable to create backend: %s", err)
	}

	logicaltest.Test(t, logicaltest.TestCase{
		CredentialBackend: b,
		Steps: []logicaltest.TestStep{
			testAccStepUser(t, "web", "password", "foo"),
			testAccStepReadUser(t, "web", "foo"),
			testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
			testUpdatePolicies(t, "web", "foo,bar"),
			testAccStepReadUser(t, "web", "bar,foo"),
			testAccStepLogin(t, "web", "password", []string{"bar", "default", "foo"}),
		},
	})
}

func testUpdatePassword(t *testing.T, user, password string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.UpdateOperation,
		Path:      "users/" + user + "/password",
		Data: map[string]interface{}{
			"password": password,
		},
	}
}

func testUpdatePolicies(t *testing.T, user, policies string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.UpdateOperation,
		Path:      "users/" + user + "/policies",
		Data: map[string]interface{}{
			"policies": policies,
		},
	}
}

func testAccStepList(t *testing.T, users []string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.ListOperation,
		Path:      "users",
		Check: func(resp *logical.Response) error {
			if resp.IsError() {
				return fmt.Errorf("got error response: %#v", *resp)
			}

			exp := []string{"web", "web2", "web3"}
			if !reflect.DeepEqual(exp, resp.Data["keys"].([]string)) {
				return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", exp, resp.Data["keys"])
			}
			return nil
		},
	}
}

func testAccStepLogin(t *testing.T, user string, pass string, policies []string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.UpdateOperation,
		Path:      "login/" + user,
		Data: map[string]interface{}{
			"password": pass,
		},
		Unauthenticated: true,

		Check:     logicaltest.TestCheckAuth(policies),
		ConnState: &tls.ConnectionState{},
	}
}

func testUserCreateOperation(
	t *testing.T, name string, password string, policies string,
) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.CreateOperation,
		Path:      "users/" + name,
		Data: map[string]interface{}{
			"password": password,
			"policies": policies,
		},
	}
}

func testAccStepUser(
	t *testing.T, name string, password string, policies string,
) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.UpdateOperation,
		Path:      "users/" + name,
		Data: map[string]interface{}{
			"password": password,
			"policies": policies,
		},
	}
}

func testAccStepDeleteUser(t *testing.T, n string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.DeleteOperation,
		Path:      "users/" + n,
	}
}

func testAccStepReadUser(t *testing.T, name string, policies string) logicaltest.TestStep {
	return logicaltest.TestStep{
		Operation: logical.ReadOperation,
		Path:      "users/" + name,
		Check: func(resp *logical.Response) error {
			if resp == nil {
				if policies == "" {
					return nil
				}

				return fmt.Errorf("bad: %#v", resp)
			}

			var d struct {
				Policies []string `mapstructure:"policies"`
			}
			if err := mapstructure.Decode(resp.Data, &d); err != nil {
				return err
			}

			if !reflect.DeepEqual(d.Policies, policyutil.ParsePolicies(policies)) {
				return fmt.Errorf("bad: %#v", resp)
			}

			return nil
		},
	}
}

func TestBackend_UserUpgrade(t *testing.T) {
	s := &logical.InmemStorage{}

	config := logical.TestBackendConfig()
	config.StorageView = s

	ctx := context.Background()

	b := Backend()
	if b == nil {
		t.Fatalf("failed to create backend")
	}
	if err := b.Setup(ctx, config); err != nil {
		t.Fatal(err)
	}

	foo := &UserEntry{
		Policies:   []string{"foo"},
		TTL:        time.Second,
		MaxTTL:     time.Second,
		BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
	}

	entry, err := logical.StorageEntryJSON("user/foo", foo)
	if err != nil {
		t.Fatal(err)
	}
	err = s.Put(ctx, entry)
	if err != nil {
		t.Fatal(err)
	}

	userEntry, err := b.user(ctx, s, "foo")
	if err != nil {
		t.Fatal(err)
	}

	exp := &UserEntry{
		Policies:   []string{"foo"},
		TTL:        time.Second,
		MaxTTL:     time.Second,
		BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
		TokenParams: tokenutil.TokenParams{
			TokenPolicies:   []string{"foo"},
			TokenTTL:        time.Second,
			TokenMaxTTL:     time.Second,
			TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
		},
	}
	if diff := deep.Equal(userEntry, exp); diff != nil {
		t.Fatal(diff)
	}
}
