// Package integration provides helpers for integration testing.
package integration

import (
	"bytes"
	"context"
	_ "embed"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/brianvoe/gofakeit/v6"
	"github.com/zitadel/logging"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/proto"

	http_util "github.com/zitadel/zitadel/internal/api/http"
	"github.com/zitadel/zitadel/internal/webauthn"
	"github.com/zitadel/zitadel/pkg/grpc/admin"
	"github.com/zitadel/zitadel/pkg/grpc/auth"
	"github.com/zitadel/zitadel/pkg/grpc/instance"
	"github.com/zitadel/zitadel/pkg/grpc/management"
	"github.com/zitadel/zitadel/pkg/grpc/org"
	"github.com/zitadel/zitadel/pkg/grpc/system"
	"github.com/zitadel/zitadel/pkg/grpc/user"
	user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)

// NotEmpty can be used as placeholder, when the returned values is unknown.
// It can be used in tests to assert whether a value should be empty or not.
const NotEmpty = "not empty"

const (
	adminPATFile = "admin-pat.txt"
)

// UserType provides constants that give
// a short explanation with the purpose
// a service user.
// This allows to pre-create users with
// different permissions and reuse them.
type UserType int

//go:generate enumer -type UserType -transform snake -trimprefix UserType
const (
	UserTypeUnspecified UserType = iota
	UserTypeIAMOwner
	UserTypeOrgOwner
	UserTypeLogin
)

const (
	UserPassword = "VeryS3cret!"
)

const (
	PortMilestoneServer = "8081"
	PortQuotaServer     = "8082"
)

// User information with a Personal Access Token.
type User struct {
	ID       string
	Username string
	Token    string
}

type UserMap map[UserType]*User

func (m UserMap) Set(typ UserType, user *User) {
	m[typ] = user
}

func (m UserMap) Get(typ UserType) *User {
	return m[typ]
}

// Host returns the primary host of zitadel, on which the first instance is served.
// http://localhost:8080 by default
func (c *Config) Host() string {
	return fmt.Sprintf("%s:%d", c.Hostname, c.Port)
}

// Instance is a Zitadel server and client with all resources available for testing.
type Instance struct {
	Config      Config
	Domain      string
	Instance    *instance.InstanceDetail
	DefaultOrg  *org.Org
	Users       UserMap
	AdminUserID string // First human user for password login

	Client   *Client
	WebAuthN *webauthn.Client
}

// GetFirstInstance returns the default instance and org information,
// with authorized machine users.
// Using the first instance is not recommended as parallel test might
// interfere with each other.
// It is recommended to use [NewInstance] instead.
func GetFirstInstance(ctx context.Context) *Instance {
	i := &Instance{
		Config: loadedConfig,
		Domain: loadedConfig.Hostname,
	}
	token := loadInstanceOwnerPAT()
	i.setClient(ctx)
	i.setupInstance(ctx, token)
	return i
}

// NewInstance returns a new instance that can be used for integration tests.
// The instance contains a gRPC client connected to the domain of this instance.
// The included users are the IAM_OWNER, ORG_OWNER of the default org and
// a Login client user.
//
// The instance is isolated and is safe for parallel testing.
func NewInstance(ctx context.Context) *Instance {
	primaryDomain := RandString(5) + ".integration.localhost"

	ctx = WithSystemAuthorization(ctx)
	resp, err := SystemClient().CreateInstance(ctx, &system.CreateInstanceRequest{
		InstanceName: "testinstance",
		CustomDomain: primaryDomain,
		Owner: &system.CreateInstanceRequest_Machine_{
			Machine: &system.CreateInstanceRequest_Machine{
				UserName:            "owner",
				Name:                "owner",
				PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{},
			},
		},
	})
	if err != nil {
		panic(err)
	}
	i := &Instance{
		Config: loadedConfig,
		Domain: primaryDomain,
	}
	i.setClient(ctx)
	i.awaitFirstUser(WithAuthorizationToken(ctx, resp.GetPat()))
	i.setupInstance(ctx, resp.GetPat())
	return i
}

func (i *Instance) ID() string {
	return i.Instance.GetId()
}

func (i *Instance) awaitFirstUser(ctx context.Context) {
	var allErrs []error
	for {
		resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
			Username: proto.String("zitadel-admin@zitadel.localhost"),
			Profile: &user_v2.SetHumanProfile{
				GivenName:  "hodor",
				FamilyName: "hodor",
				NickName:   proto.String("hodor"),
			},
			Email: &user_v2.SetHumanEmail{
				Email: "zitadel-admin@zitadel.localhost",
				Verification: &user_v2.SetHumanEmail_IsVerified{
					IsVerified: true,
				},
			},
			PasswordType: &user_v2.AddHumanUserRequest_Password{
				Password: &user_v2.Password{
					Password:       "Password1!",
					ChangeRequired: false,
				},
			},
		})
		if err == nil {
			i.AdminUserID = resp.GetUserId()
			return
		}
		logging.WithError(err).Debug("await first instance user")
		allErrs = append(allErrs, err)
		select {
		case <-ctx.Done():
			panic(errors.Join(append(allErrs, ctx.Err())...))
		case <-time.After(time.Second):
			continue
		}
	}
}

func (i *Instance) setupInstance(ctx context.Context, token string) {
	i.Users = make(UserMap)
	ctx = WithAuthorizationToken(ctx, token)
	i.setInstance(ctx)
	i.setOrganization(ctx)
	i.createMachineUserInstanceOwner(ctx, token)
	i.createMachineUserOrgOwner(ctx)
	i.createLoginClient(ctx)
	i.createWebAuthNClient()
}

// Host returns the primary Domain of the instance with the port.
func (i *Instance) Host() string {
	return fmt.Sprintf("%s:%d", i.Domain, i.Config.Port)
}

func loadInstanceOwnerPAT() string {
	data, err := os.ReadFile(filepath.Join(tmpDir, adminPATFile))
	if err != nil {
		panic(err)
	}
	return string(bytes.TrimSpace(data))
}

func (i *Instance) createMachineUserInstanceOwner(ctx context.Context, token string) {
	mustAwait(func() error {
		user, err := i.Client.Auth.GetMyUser(WithAuthorizationToken(ctx, token), &auth.GetMyUserRequest{})
		if err != nil {
			return err
		}
		i.Users.Set(UserTypeIAMOwner, &User{
			ID:       user.GetUser().GetId(),
			Username: user.GetUser().GetUserName(),
			Token:    token,
		})
		return nil
	})
}

func (i *Instance) createMachineUserOrgOwner(ctx context.Context) {
	_, err := i.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
		UserId: i.createMachineUser(ctx, UserTypeOrgOwner),
		Roles:  []string{"ORG_OWNER"},
	})
	if err != nil {
		panic(err)
	}
}

func (i *Instance) createLoginClient(ctx context.Context) {
	i.createMachineUser(ctx, UserTypeLogin)
}

func (i *Instance) setClient(ctx context.Context) {
	client, err := newClient(ctx, i.Host())
	if err != nil {
		panic(err)
	}
	i.Client = client
}

func (i *Instance) setInstance(ctx context.Context) {
	mustAwait(func() error {
		instance, err := i.Client.Admin.GetMyInstance(ctx, &admin.GetMyInstanceRequest{})
		i.Instance = instance.GetInstance()
		return err
	})
}

func (i *Instance) setOrganization(ctx context.Context) {
	mustAwait(func() error {
		resp, err := i.Client.Mgmt.GetMyOrg(ctx, &management.GetMyOrgRequest{})
		i.DefaultOrg = resp.GetOrg()
		return err
	})
}

func (i *Instance) createMachineUser(ctx context.Context, userType UserType) (userID string) {
	mustAwait(func() error {
		username := gofakeit.Username()
		userResp, err := i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
			UserName:        username,
			Name:            username,
			Description:     userType.String(),
			AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
		})
		if err != nil {
			return err
		}
		userID = userResp.GetUserId()
		patResp, err := i.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{
			UserId: userID,
		})
		if err != nil {
			return err
		}
		i.Users.Set(userType, &User{
			ID:       userID,
			Username: username,
			Token:    patResp.GetToken(),
		})
		return nil
	})
	return userID
}

func (i *Instance) createWebAuthNClient() {
	i.WebAuthN = webauthn.NewClient(i.Config.WebAuthNName, i.Domain, http_util.BuildOrigin(i.Host(), i.Config.Secure))
}

func (i *Instance) WithAuthorization(ctx context.Context, u UserType) context.Context {
	return i.WithInstanceAuthorization(ctx, u)
}

func (i *Instance) WithInstanceAuthorization(ctx context.Context, u UserType) context.Context {
	return WithAuthorizationToken(ctx, i.Users.Get(u).Token)
}

func (i *Instance) GetUserID(u UserType) string {
	return i.Users.Get(u).ID
}

func WithAuthorizationToken(ctx context.Context, token string) context.Context {
	md, ok := metadata.FromOutgoingContext(ctx)
	if !ok {
		md = make(metadata.MD)
	}
	md.Set("Authorization", fmt.Sprintf("Bearer %s", token))
	return metadata.NewOutgoingContext(ctx, md)
}

func (i *Instance) BearerToken(ctx context.Context) string {
	md, ok := metadata.FromOutgoingContext(ctx)
	if !ok {
		return ""
	}
	return md.Get("Authorization")[0]
}

func (i *Instance) WithSystemAuthorizationHTTP(u UserType) map[string]string {
	return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", i.Users.Get(u).Token)}
}

func await(af func() error) error {
	maxTimer := time.NewTimer(15 * time.Minute)
	for {
		err := af()
		if err == nil {
			return nil
		}
		select {
		case <-maxTimer.C:
			return err
		case <-time.After(time.Second):
			continue
		}
	}
}

func mustAwait(af func() error) {
	if err := await(af); err != nil {
		panic(err)
	}
}