package approvals

import (
	"context"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"testing"
	"time"

	_ "github.com/jinzhu/gorm/dialects/sqlite"

	"github.com/keel-hq/keel/pkg/store/sql"
	"github.com/keel-hq/keel/types"
)

func NewTestingUtils() (*sql.SQLStore, func()) {
	dir, err := ioutil.TempDir("", "whstoretest")
	if err != nil {
		log.Fatal(err)
	}
	tmpfn := filepath.Join(dir, "gorm.db")
	// defer
	store, err := sql.New(sql.Opts{DatabaseType: "sqlite3", URI: tmpfn})
	if err != nil {
		log.Fatal(err)
	}

	teardown := func() {
		os.RemoveAll(dir) // clean up
	}

	return store, teardown
}

func TestCreateApproval(t *testing.T) {

	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	stored, err := am.Get("xxx/app-1")
	if err != nil {
		t.Fatalf("failed to get approval: %s", err)
	}

	if stored.CurrentVersion != "1.2.3" {
		t.Errorf("unexpected version: %s", stored.CurrentVersion)
	}
}

func TestDeleteApproval(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	stored, err := am.Get("xxx/app-1")
	if err != nil {
		t.Fatalf("didn't find approval: %s", err)
	}

	err = am.Delete(stored)
	if err != nil {
		t.Errorf("failed to delete approval: %s", err)
	}

	_, err = am.Get("xxx/app-1")
	if err == nil {
		t.Errorf("expected to get an error when retrieving deleted approval")
	}

}

func TestUpdateApproval(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		VotesRequired:  1,
		VotesReceived:  0,
		Deadline:       time.Now().Add(5 * time.Minute),
		Event: &types.Event{
			Repository: types.Repository{
				Name: "very/repo",
				Tag:  "1.2.5",
			},
		},
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ch, err := am.SubscribeApproved(ctx)
	if err != nil {
		t.Fatalf("failed to subscribe: %s", err)
	}

	err = am.Update(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		VotesRequired:  1,
		VotesReceived:  1,
		Deadline:       time.Now().Add(5 * time.Minute),
		Event: &types.Event{
			Repository: types.Repository{
				Name: "very/repo",
				Tag:  "1.2.5",
			},
		},
	})

	approved := <-ch

	if approved.Event.Repository.Name != "very/repo" {
		t.Errorf("unexpected repo name in re-submitted event: %s", approved.Event.Repository.Name)
	}
	if approved.Event.Repository.Tag != "1.2.5" {
		t.Errorf("unexpected repo tag in re-submitted event: %s", approved.Event.Repository.Tag)
	}
}

func TestUpdateApprovalRejected(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		VotesRequired:  1,
		VotesReceived:  0,
		Deadline:       time.Now().Add(5 * time.Minute),
		Event: &types.Event{
			Repository: types.Repository{
				Name: "very/repo",
				Tag:  "1.2.5",
			},
		},
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	created, _ := am.Get("xxx/app-1")

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ch, err := am.SubscribeApproved(ctx)
	if err != nil {
		t.Fatalf("failed to subscribe: %s", err)
	}

	// rejecting
	err = am.Update(&types.Approval{
		ID:             created.ID,
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		VotesRequired:  1,
		VotesReceived:  0,
		Rejected:       true,
		Deadline:       time.Now().Add(5 * time.Minute),
		Event: &types.Event{
			Repository: types.Repository{
				Name: "very/repo",
				Tag:  "1.2.5",
			},
		},
	})
	if err != nil {
		t.Fatalf("failed to update approval: %s", err)
	}

	// sending vote
	err = am.Update(&types.Approval{
		ID:             created.ID,
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		VotesRequired:  1,
		VotesReceived:  1,
		Rejected:       true,
		Deadline:       time.Now().Add(5 * time.Minute),
		Event: &types.Event{
			Repository: types.Repository{
				Name: "very/repo",
				Tag:  "1.2.5",
			},
		},
	})
	if err != nil {
		t.Fatalf("failed to update approval: %s", err)
	}

	select {
	case <-time.After(500 * time.Millisecond):
		// success
		return
	case approval := <-ch:
		t.Errorf("unexpected approval got: %s", approval.Identifier)
	}

}

func TestApprove(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1:1.2.5",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	am.Approve("xxx/app-1:1.2.5", "warda")

	stored, err := am.Get("xxx/app-1:1.2.5")
	if err != nil {
		t.Fatalf("failed to get approval: %s", err)
	}

	if stored.VotesReceived != 1 {
		t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
	}
}

func TestApproveTwiceSameVoter(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})
	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1:1.2.5",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	am.Approve("xxx/app-1:1.2.5", "warda")
	am.Approve("xxx/app-1:1.2.5", "warda")

	stored, err := am.Get("xxx/app-1:1.2.5")
	if err != nil {
		t.Fatalf("failed to get approval: %s", err)
	}

	// should still be the same
	if stored.VotesReceived != 1 {
		t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
	}
}

func TestApproveTwoVoters(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1:1.2.5",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	am.Approve("xxx/app-1:1.2.5", "w")
	am.Approve("xxx/app-1:1.2.5", "k")

	stored, err := am.Get("xxx/app-1:1.2.5")
	if err != nil {
		t.Fatalf("failed to get approval: %s", err)
	}

	// should still be the same
	if stored.VotesReceived != 2 {
		t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
	}
}

func TestReject(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(5 * time.Minute),
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	am.Reject("xxx/app-1")

	stored, err := am.Get("xxx/app-1")
	if err != nil {
		t.Fatalf("failed to get approval: %s", err)
	}

	if !stored.Rejected {
		t.Errorf("unexpected approval to be rejected")
	}
}

func TestExpire(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Deadline:       time.Now().Add(-5 * time.Minute),
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create approval: %s", err)
	}

	err = am.expireEntries()
	if err != nil {
		t.Errorf("got error while expiring entries: %s", err)
	}

	_, err = am.Get("xxx/app-1")
	if err == nil {
		t.Errorf("expected approval to be deleted but didn't get an error")
	}
}

func TestGetArchived(t *testing.T) {
	store, teardown := NewTestingUtils()
	defer teardown()

	am := New(&Opts{
		Store: store,
	})

	err := am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Archived:       true,
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create first approval: %s", err)
	}

	err = am.Create(&types.Approval{
		Provider:       types.ProviderTypeKubernetes,
		Identifier:     "xxx/app-1",
		CurrentVersion: "1.2.3",
		NewVersion:     "1.2.5",
		Archived:       false,
		VotesRequired:  2,
		VotesReceived:  0,
	})

	if err != nil {
		t.Fatalf("failed to create  second (not archived )approval: %s", err)
	}

	secondStore, err := am.Get("xxx/app-1")
	if err != nil {
		t.Errorf("expected approval to be deleted but didn't get an error")
	}
	if secondStore.Archived {
		t.Errorf("didn't expect approval to be archived")
	}
}
