package tests

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/influxdata/flux/csv"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/authorization"
	influxhttp "github.com/influxdata/influxdb/v2/http"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/pkg/httpc"
	"github.com/influxdata/influxdb/v2/tenant"
)

type ClientConfig struct {
	UserID             platform.ID
	OrgID              platform.ID
	BucketID           platform.ID
	DocumentsNamespace string

	// If Session is provided, Token is ignored.
	Token   string
	Session *influxdb.Session
}

// Client provides an API for writing, querying, and interacting with
// resources like authorizations, buckets, and organizations.
type Client struct {
	Client *httpc.Client
	*influxhttp.Service

	*authorization.AuthorizationClientService
	*tenant.BucketClientService
	*tenant.OrgClientService
	*tenant.UserClientService

	ClientConfig
}

// NewClient initialises a new Client which is ready to write points to the HTTP write endpoint.
func NewClient(endpoint string, config ClientConfig) (*Client, error) {
	opts := make([]httpc.ClientOptFn, 0)
	if config.Session != nil {
		config.Token = ""
		opts = append(opts, httpc.WithSessionCookie(config.Session.Key))
	}
	hc, err := influxhttp.NewHTTPClient(endpoint, config.Token, false, opts...)
	if err != nil {
		return nil, err
	}

	svc, err := influxhttp.NewService(hc, endpoint, config.Token)
	if err != nil {
		return nil, err
	}
	return &Client{
		Client:                     hc,
		Service:                    svc,
		AuthorizationClientService: &authorization.AuthorizationClientService{Client: hc},
		BucketClientService:        &tenant.BucketClientService{Client: hc},
		OrgClientService:           &tenant.OrgClientService{Client: hc},
		UserClientService:          &tenant.UserClientService{Client: hc},
		ClientConfig:               config,
	}, nil
}

// Open opens the client
func (c *Client) Open() error { return nil }

// Close closes the client
func (c *Client) Close() error { return nil }

// MustWriteBatch calls WriteBatch, panicking if an error is encountered.
func (c *Client) MustWriteBatch(points string) {
	if err := c.WriteBatch(points); err != nil {
		panic(err)
	}
}

// WriteBatch writes the current batch of points to the HTTP endpoint.
func (c *Client) WriteBatch(points string) error {
	return c.WriteService.WriteTo(
		context.Background(),
		influxdb.BucketFilter{
			ID:             &c.BucketID,
			OrganizationID: &c.OrgID,
		},
		strings.NewReader(points),
	)
}

// Query returns the CSV response from a flux query to the HTTP API.
//
// This also remove all the \r to make it easier to write tests.
func (c *Client) QueryFlux(org, query string) (string, error) {
	var csv string
	csvResp := func(resp *http.Response) error {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return err
		}
		// remove the \r to simplify testing against a body of CSV.
		body = bytes.ReplaceAll(body, []byte("\r"), nil)
		csv = string(body)
		return nil
	}

	qr := QueryRequestBody(query)
	err := c.Client.PostJSON(qr, fluxPath).
		QueryParams([2]string{"org", org}).
		Accept("text/csv").
		RespFn(csvResp).
		StatusFn(httpc.StatusIn(http.StatusOK)).
		Do(context.Background())

	return csv, err
}

const (
	fluxPath = "/api/v2/query"
	// This is the only namespace for documents present after init.
	DefaultDocumentsNamespace = "templates"
)

// QueryRequestBody creates a body for a flux query using common CSV output params.
// Headers are included, but, annotations are not.
func QueryRequestBody(flux string) *influxhttp.QueryRequest {
	header := true
	return &influxhttp.QueryRequest{
		Type:  "flux",
		Query: flux,
		Dialect: influxhttp.QueryDialect{
			Header:         &header,
			Delimiter:      ",",
			CommentPrefix:  "#",
			DateTimeFormat: "RFC3339",
			Annotations:    csv.DefaultEncoderConfig().Annotations,
		},
	}
}

// MustCreateAuth creates an auth  or is a fatal error.
// Used in tests where the content of the bucket does not matter.
//
// This authorization token is an operator token for the default
// organization for the default user.
func (c *Client) MustCreateAuth(t *testing.T) platform.ID {
	t.Helper()

	perms := influxdb.OperPermissions()
	auth := &influxdb.Authorization{
		OrgID:       c.OrgID,
		UserID:      c.UserID,
		Permissions: perms,
	}
	err := c.CreateAuthorization(context.Background(), auth)
	if err != nil {
		t.Fatalf("unable to create auth: %v", err)
	}
	return auth.ID
}

// MustCreateBucket creates a bucket or is a fatal error.
// Used in tests where the content of the bucket does not matter.
func (c *Client) MustCreateBucket(t *testing.T) platform.ID {
	t.Helper()

	bucket := &influxdb.Bucket{OrgID: c.OrgID, Name: "n1"}
	err := c.CreateBucket(context.Background(), bucket)
	if err != nil {
		t.Fatalf("unable to create bucket: %v", err)
	}
	return bucket.ID
}

// MustCreateOrg creates an org or is a fatal error.
// Used in tests where the content of the org does not matter.
func (c *Client) MustCreateOrg(t *testing.T) platform.ID {
	t.Helper()

	org := &influxdb.Organization{Name: "n1"}
	err := c.CreateOrganization(context.Background(), org)
	if err != nil {
		t.Fatalf("unable to create org: %v", err)
	}
	return org.ID
}

// MustCreateLabel creates a label or is a fatal error.
// Used in tests where the content of the label does not matter.
func (c *Client) MustCreateLabel(t *testing.T) platform.ID {
	t.Helper()

	l := &influxdb.Label{OrgID: c.OrgID, Name: "n1"}
	err := c.CreateLabel(context.Background(), l)
	if err != nil {
		t.Fatalf("unable to create label: %v", err)
	}
	return l.ID
}

// MustCreateCheck creates a check or is a fatal error.
// Used in tests where the content of the check does not matter.
func (c *Client) MustCreateCheck(t *testing.T) platform.ID {
	t.Helper()

	chk, err := c.CreateCheck(context.Background(), MockCheck("c", c.OrgID, c.UserID))
	if err != nil {
		t.Fatalf("unable to create check: %v", err)
	}
	return chk.ID
}

// MustCreateTelegraf creates a telegraf config or is a fatal error.
// Used in tests where the content of the telegraf config does not matter.
func (c *Client) MustCreateTelegraf(t *testing.T) platform.ID {
	t.Helper()

	tc := &influxdb.TelegrafConfig{
		OrgID:       c.OrgID,
		Name:        "n1",
		Description: "d1",
		Config:      "[[howdy]]",
	}
	unused := platform.ID(1) /* this id is not used in the API */
	err := c.CreateTelegrafConfig(context.Background(), tc, unused)
	if err != nil {
		t.Fatalf("unable to create telegraf config: %v", err)
	}
	return tc.ID
}

// MustCreateUser creates a user or is a fatal error.
// Used in tests where the content of the user does not matter.
func (c *Client) MustCreateUser(t *testing.T) platform.ID {
	t.Helper()

	u := &influxdb.User{Name: "n1"}
	err := c.CreateUser(context.Background(), u)
	if err != nil {
		t.Fatalf("unable to create user: %v", err)
	}
	return u.ID
}

// MustCreateVariable creates a variable or is a fatal error.
// Used in tests where the content of the variable does not matter.
func (c *Client) MustCreateVariable(t *testing.T) platform.ID {
	t.Helper()

	v := &influxdb.Variable{
		OrganizationID: c.OrgID,
		Name:           "n1",
		Arguments: &influxdb.VariableArguments{
			Type:   "constant",
			Values: influxdb.VariableConstantValues{"v1", "v2"},
		},
	}
	err := c.CreateVariable(context.Background(), v)
	if err != nil {
		t.Fatalf("unable to create variable: %v", err)
	}
	return v.ID
}

// MustCreateNotificationEndpoint creates a notification endpoint or is a fatal error.
// Used in tests where the content of the notification endpoint does not matter.
func (c *Client) MustCreateNotificationEndpoint(t *testing.T) platform.ID {
	t.Helper()

	ne := ValidNotificationEndpoint(c.OrgID)
	err := c.CreateNotificationEndpoint(context.Background(), ne, c.UserID)
	if err != nil {
		t.Fatalf("unable to create notification endpoint: %v", err)
	}
	return ne.GetID()
}

// MustCreateNotificationRule creates a Notification Rule or is a fatal error
// Used in tests where the content of the notification rule does not matter
func (c *Client) MustCreateNotificationRule(t *testing.T) platform.ID {
	t.Helper()
	ctx := context.Background()

	ne := ValidCustomNotificationEndpoint(c.OrgID, time.Now().String())
	err := c.CreateNotificationEndpoint(ctx, ne, c.UserID)
	if err != nil {
		t.Fatalf("unable to create notification endpoint: %v", err)
	}
	endpointID := ne.GetID()
	r := ValidNotificationRule(c.OrgID, endpointID)
	rc := influxdb.NotificationRuleCreate{NotificationRule: r, Status: influxdb.Active}

	err = c.CreateNotificationRule(ctx, rc, c.UserID)
	if err != nil {
		t.Fatalf("unable to create notification rule: %v", err)
	}

	// we don't need this endpoint, so delete it to be compatible with other tests
	_, _, err = c.DeleteNotificationEndpoint(ctx, endpointID)
	if err != nil {
		t.Fatalf("unable to delete notification endpoint: %v", err)
	}

	return r.GetID()
}

// MustCreateDBRPMapping creates a DBRP Mapping or is a fatal error.
// Used in tests where the content of the mapping does not matter.
// The created mapping points to the user's default bucket.
func (c *Client) MustCreateDBRPMapping(t *testing.T) platform.ID {
	t.Helper()
	ctx := context.Background()

	m := &influxdb.DBRPMapping{
		Database:        "db",
		RetentionPolicy: "rp",
		OrganizationID:  c.OrgID,
		BucketID:        c.BucketID,
	}
	if err := c.DBRPMappingService.Create(ctx, m); err != nil {
		t.Fatalf("unable to create DBRP mapping: %v", err)
	}
	return m.ID
}

// MustCreateResource will create a generic resource via the API.
// Used in tests where the content of the resource does not matter.
//
//	// Create one of each org resource
//	for _, r := range influxdb.OrgResourceTypes {
//	    client.MustCreateResource(t, r)
//	}
//
//
//	// Create a variable:
//	id := client.MustCreateResource(t, influxdb.VariablesResourceType)
//	defer client.MustDeleteResource(t, influxdb.VariablesResourceType, id)
func (c *Client) MustCreateResource(t *testing.T, r influxdb.ResourceType) platform.ID {
	t.Helper()

	switch r {
	case influxdb.AuthorizationsResourceType: // 0
		return c.MustCreateAuth(t)
	case influxdb.BucketsResourceType: // 1
		return c.MustCreateBucket(t)
	case influxdb.OrgsResourceType: // 3
		return c.MustCreateOrg(t)
	case influxdb.SourcesResourceType: // 4
		t.Skip("I think sources are going to be removed right?")
	case influxdb.TasksResourceType: // 5
		t.Skip("Task go client is not yet created")
	case influxdb.TelegrafsResourceType: // 6
		return c.MustCreateTelegraf(t)
	case influxdb.UsersResourceType: // 7
		return c.MustCreateUser(t)
	case influxdb.VariablesResourceType: // 8
		return c.MustCreateVariable(t)
	case influxdb.ScraperResourceType: // 9
		t.Skip("Scraper go client is not yet created")
	case influxdb.SecretsResourceType: // 10
		t.Skip("Secrets go client is not yet created")
	case influxdb.LabelsResourceType: // 11
		return c.MustCreateLabel(t)
	case influxdb.ViewsResourceType: // 12
		t.Skip("Are views still a thing?")
	case influxdb.NotificationRuleResourceType: // 14
		return c.MustCreateNotificationRule(t)
	case influxdb.NotificationEndpointResourceType: // 15
		return c.MustCreateNotificationEndpoint(t)
	case influxdb.ChecksResourceType: // 16
		return c.MustCreateCheck(t)
	case influxdb.DBRPResourceType: // 17
		return c.MustCreateDBRPMapping(t)
	}
	return 0
}

// DeleteResource will remove a resource using the API.
func (c *Client) DeleteResource(t *testing.T, r influxdb.ResourceType, id platform.ID) error {
	t.Helper()

	ctx := context.Background()
	switch r {
	case influxdb.AuthorizationsResourceType: // 0
		return c.DeleteAuthorization(ctx, id)
	case influxdb.BucketsResourceType: // 1
		return c.DeleteBucket(context.Background(), id)
	case influxdb.OrgsResourceType: // 3
		return c.DeleteOrganization(ctx, id)
	case influxdb.SourcesResourceType: // 4
		t.Skip("I think sources are going to be removed right?")
	case influxdb.TasksResourceType: // 5
		t.Skip("Task go client is not yet created")
	case influxdb.TelegrafsResourceType: // 6
		return c.DeleteTelegrafConfig(ctx, id)
	case influxdb.UsersResourceType: // 7
		return c.DeleteUser(ctx, id)
	case influxdb.VariablesResourceType: // 8
		return c.DeleteVariable(ctx, id)
	case influxdb.ScraperResourceType: // 9
		t.Skip("Scraper go client is not yet created")
	case influxdb.SecretsResourceType: // 10
		t.Skip("Secrets go client is not yet created")
	case influxdb.LabelsResourceType: // 11
		return c.DeleteLabel(ctx, id)
	case influxdb.ViewsResourceType: // 12
		t.Skip("Are views still a thing?")
	case influxdb.NotificationRuleResourceType: // 14
		return c.DeleteNotificationRule(ctx, id)
	case influxdb.NotificationEndpointResourceType: // 15
		// Ignore the other results as suggested by goDoc.
		_, _, err := c.DeleteNotificationEndpoint(ctx, id)
		return err
	case influxdb.ChecksResourceType: // 16
		return c.DeleteCheck(ctx, id)
	case influxdb.DBRPResourceType: // 17
		return c.DBRPMappingService.Delete(ctx, c.OrgID, id)
	}
	return nil
}

// MustDeleteResource requires no error when deleting a resource.
func (c *Client) MustDeleteResource(t *testing.T, r influxdb.ResourceType, id platform.ID) {
	t.Helper()

	if err := c.DeleteResource(t, r, id); err != nil {
		t.Fatalf("unable to delete resource %v %v: %v", r, id, err)
	}
}

// FindAll returns all the IDs of a specific resource type.
func (c *Client) FindAll(t *testing.T, r influxdb.ResourceType) ([]platform.ID, error) {
	t.Helper()

	var ids []platform.ID
	ctx := context.Background()
	switch r {
	case influxdb.AuthorizationsResourceType: // 0
		rs, _, err := c.FindAuthorizations(ctx, influxdb.AuthorizationFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.BucketsResourceType: // 1
		rs, _, err := c.FindBuckets(ctx, influxdb.BucketFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.OrgsResourceType: // 3
		rs, _, err := c.FindOrganizations(ctx, influxdb.OrganizationFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.SourcesResourceType: // 4
		t.Skip("I think sources are going to be removed right?")
	case influxdb.TasksResourceType: // 5
		t.Skip("Task go client is not yet created")
	case influxdb.TelegrafsResourceType: // 6
		rs, _, err := c.FindTelegrafConfigs(ctx, influxdb.TelegrafConfigFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.UsersResourceType: // 7
		rs, _, err := c.FindUsers(ctx, influxdb.UserFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.VariablesResourceType: // 8
		rs, err := c.FindVariables(ctx, influxdb.VariableFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.ScraperResourceType: // 9
		t.Skip("Scraper go client is not yet created")
	case influxdb.SecretsResourceType: // 10
		t.Skip("Secrets go client is not yet created")
	case influxdb.LabelsResourceType: // 11
		rs, err := c.FindLabels(ctx, influxdb.LabelFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.ViewsResourceType: // 12
		t.Skip("Are views still a thing?")
	case influxdb.NotificationRuleResourceType: // 14
		rs, _, err := c.FindNotificationRules(ctx, influxdb.NotificationRuleFilter{OrgID: &c.OrgID})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.GetID())
		}
		return ids, nil
	case influxdb.NotificationEndpointResourceType: // 15
		rs, _, err := c.FindNotificationEndpoints(ctx, influxdb.NotificationEndpointFilter{OrgID: &c.OrgID})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.GetID())
		}
	case influxdb.ChecksResourceType: // 16
		rs, _, err := c.FindChecks(ctx, influxdb.CheckFilter{})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	case influxdb.DBRPResourceType: // 17
		rs, _, err := c.DBRPMappingService.FindMany(ctx, influxdb.DBRPMappingFilter{OrgID: &c.OrgID})
		if err != nil {
			return nil, err
		}
		for _, r := range rs {
			ids = append(ids, r.ID)
		}
	}
	return ids, nil
}

// MustFindAll returns all the IDs of a specific resource type; any error
// is fatal.
func (c *Client) MustFindAll(t *testing.T, r influxdb.ResourceType) []platform.ID {
	t.Helper()

	ids, err := c.FindAll(t, r)
	if err != nil {
		t.Fatalf("unexpected error finding resources %v: %v", r, err)
	}
	return ids
}

func (c *Client) AddURM(u platform.ID, typ influxdb.UserType, r influxdb.ResourceType, id platform.ID) error {
	access := &influxdb.UserResourceMapping{
		UserID:       u,
		UserType:     typ,
		MappingType:  influxdb.UserMappingType,
		ResourceType: r,
		ResourceID:   id,
	}

	return c.CreateUserResourceMapping(
		context.Background(),
		access,
	)
}

// AddOwner associates the user as owner of the resource.
func (c *Client) AddOwner(user platform.ID, r influxdb.ResourceType, id platform.ID) error {
	return c.AddURM(user, influxdb.Owner, r, id)
}

// MustAddOwner requires that the user is associated with the resource
// or the test will be stopped fatally.
func (c *Client) MustAddOwner(t *testing.T, user platform.ID, r influxdb.ResourceType, id platform.ID) {
	t.Helper()

	if err := c.AddOwner(user, r, id); err != nil {
		t.Fatalf("unexpected error adding owner %v to %v: %v", user, id, err)
	}
}

// AddMember associates the user as member of the resource.
func (c *Client) AddMember(user platform.ID, r influxdb.ResourceType, id platform.ID) error {
	return c.AddURM(user, influxdb.Member, r, id)
}

// MustAddMember requires that the user is associated with the resource
// or the test will be stopped fatally.
func (c *Client) MustAddMember(t *testing.T, user platform.ID, r influxdb.ResourceType, id platform.ID) {
	t.Helper()

	if err := c.AddMember(user, r, id); err != nil {
		t.Fatalf("unexpected error adding member %v to %v", user, id)
	}
}

// RemoveURM removes association of the user to the resource.
// Interestingly the URM service does not make difference on the user type.
// I.e. removing an URM from a user to a resource, will delete every URM of every type
// from that user to that resource.
// Or, put in another way, there can only be one resource mapping from a user to a
// resource at a time: either you are a member, or an owner (in that case you are a member too).
func (c *Client) RemoveURM(user, id platform.ID) error {
	return c.DeleteUserResourceMapping(context.Background(), id, user)
}

// RemoveSpecificURM gets around a client issue where deletes doesn't have enough context to remove a urm from
// a specific resource type
func (c *Client) RemoveSpecificURM(rt influxdb.ResourceType, ut influxdb.UserType, user, id platform.ID) error {
	return c.SpecificURMSvc(rt, ut).DeleteUserResourceMapping(context.Background(), id, user)
}

// MustRemoveURM requires that the user is removed as owner/member from the resource.
func (c *Client) MustRemoveURM(t *testing.T, user, id platform.ID) {
	t.Helper()

	if err := c.RemoveURM(user, id); err != nil {
		t.Fatalf("unexpected error removing org/resource mapping: %v", err)
	}
}

// CreateLabelMapping creates a label mapping for label `l` to the resource with `id`.
func (c *Client) CreateLabelMapping(l platform.ID, r influxdb.ResourceType, id platform.ID) error {
	mapping := &influxdb.LabelMapping{
		LabelID:      l,
		ResourceType: r,
		ResourceID:   id,
	}
	return c.LabelService.CreateLabelMapping(
		context.Background(),
		mapping,
	)
}

// MustCreateLabelMapping requires that the label is associated with the resource
// or the test will be stopped fatally.
func (c *Client) MustCreateLabelMapping(t *testing.T, l platform.ID, r influxdb.ResourceType, id platform.ID) {
	t.Helper()

	if err := c.CreateLabelMapping(l, r, id); err != nil {
		t.Fatalf("unexpected error attaching label %v to %v: %v", l, id, err)
	}
}

// FindLabelMappings finds the labels for the specified resource.
func (c *Client) FindLabelMappings(r influxdb.ResourceType, id platform.ID) ([]platform.ID, error) {
	filter := influxdb.LabelMappingFilter{
		ResourceType: r,
		ResourceID:   id,
	}
	ls, err := c.LabelService.FindResourceLabels(
		context.Background(),
		filter,
	)
	if err != nil {
		return nil, err
	}
	var ids []platform.ID
	for _, r := range ls {
		ids = append(ids, r.ID)
	}
	return ids, nil
}

// MustFindLabelMappings makes the test fail if an error is found.
func (c *Client) MustFindLabelMappings(t *testing.T, r influxdb.ResourceType, id platform.ID) []platform.ID {
	t.Helper()

	ls, err := c.FindLabelMappings(r, id)
	if err != nil {
		t.Fatalf("unexpected error finding label mappings: %v", err)
	}
	return ls
}

// DeleteLabelMapping deletes the label for the specified resource.
func (c *Client) DeleteLabelMapping(l platform.ID, r influxdb.ResourceType, id platform.ID) error {
	m := &influxdb.LabelMapping{
		ResourceType: r,
		ResourceID:   id,
		LabelID:      l,
	}
	return c.LabelService.DeleteLabelMapping(
		context.Background(),
		m,
	)
}

// MustDeleteLabelMapping makes the test fail if an error is found.
func (c *Client) MustDeleteLabelMapping(t *testing.T, l platform.ID, r influxdb.ResourceType, id platform.ID) {
	t.Helper()

	if err := c.DeleteLabelMapping(l, r, id); err != nil {
		t.Fatalf("unexpected error deleting label %v from %v", l, id)
	}
}
