package core

import (
	"context"
	"fmt"

	"github.com/imdario/mergo"

	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/vagrant-plugin-sdk/component"
	"github.com/hashicorp/vagrant-plugin-sdk/core"
	"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
	"github.com/hashicorp/vagrant/internal/server/ptypes"
	"github.com/mitchellh/go-testing-interface"
)

// TestTarget returns a fully in-memory and side-effect free Target that
// can be used for testing. Additional options can be given to provide your own
// factories, configuration, etc.
func TestTarget(t testing.T, p *Project, st *vagrant_server.Target, opts ...TestTargetOption) (target *Target) {
	testingTarget := ptypes.TestTarget(t, st)
	testingTarget.Project = p.Ref().(*vagrant_plugin_sdk.Ref_Project)
	_, err := p.basis.client.UpsertTarget(
		context.Background(),
		&vagrant_server.UpsertTargetRequest{
			Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
			Target:  testingTarget,
		},
	)
	if err != nil {
		t.Fatal(err)
	}

	target, err = p.factory.NewTarget(
		[]TargetOption{
			WithProject(p),
			WithTargetRef(
				&vagrant_plugin_sdk.Ref_Target{
					Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
					Name:    testingTarget.Name,
				},
			),
		}...,
	)

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

	for _, opt := range opts {
		if oerr := opt(target); oerr != nil {
			err = multierror.Append(err, oerr)
		}
	}
	if err != nil {
		t.Fatal(err)
	}

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

	return
}

// TestMinimalTarget uses a minimal project to setup the most basic target
// that will work for testing
func TestMinimalTarget(t testing.T) (target *Target) {
	tp := TestMinimalProject(t)
	_, err := tp.basis.client.UpsertTarget(
		context.Background(),
		&vagrant_server.UpsertTargetRequest{
			Project: tp.Ref().(*vagrant_plugin_sdk.Ref_Project),
			Target: &vagrant_server.Target{
				Name:    "test-target",
				Project: tp.Ref().(*vagrant_plugin_sdk.Ref_Project),
			},
		},
	)
	if err != nil {
		t.Fatal(err)
	}

	target, err = tp.factory.NewTarget(
		[]TargetOption{
			WithProject(tp),
			WithTargetRef(
				&vagrant_plugin_sdk.Ref_Target{
					Project: tp.Ref().(*vagrant_plugin_sdk.Ref_Project),
					Name:    "test-target",
				},
			),
		}...,
	)

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

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

	return
}

// TestMachine returns a fully in-memory and side-effect free Machine that
// can be used for testing. Additional options can be given to provide your own
// factories, configuration, etc.
func TestMachine(t testing.T, tp *Project, opts ...TestTargetOption) (machine *Machine) {
	tt := TestTarget(t, tp, &vagrant_server.Target{})
	specialized, err := tt.Specialize((*core.Machine)(nil))
	if err != nil {
		t.Fatal(err)
	}

	machine = specialized.(*Machine)
	for _, opt := range opts {
		if oerr := opt(machine); oerr != nil {
			err = multierror.Append(err, oerr)
		}
	}
	if err != nil {
		t.Fatal(err)
	}

	return
}

// TestMinimalMachine uses a minimal project to setup the most basic machine
// that will work for testing
func TestMinimalMachine(t testing.T) (machine *Machine) {
	tp := TestMinimalProject(t)
	tt := TestTarget(t, tp, &vagrant_server.Target{})
	specialized, err := tt.Specialize((*core.Machine)(nil))
	if err != nil {
		t.Fatal(err)
	}
	machine = specialized.(*Machine)
	return
}

type TestTargetOption func(interface{}) error

func WithTestTargetConfig(config *component.ConfigData) TestTargetOption {
	return func(raw interface{}) (err error) {
		switch v := raw.(type) {
		case *Target:
			return mergo.Merge(v.vagrantfile.root, config)
		case *Machine:
			return mergo.Merge(v.vagrantfile.root, config)
		default:
			panic(fmt.Sprintf("Invalid type for TestTargetOption (%T)", raw))
		}
	}
}

func WithTestTargetProvider(provider string) TestTargetOption {
	return func(raw interface{}) (err error) {
		switch v := raw.(type) {
		case *Target:
			v.target.Provider = provider
		case *Machine:
			v.target.Provider = provider
		default:
			panic(fmt.Sprintf("Invalid type for TestTargetOption (%T)", raw))
		}
		return
	}
}
