package dbrp_test

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/dbrp"
	"github.com/influxdata/influxdb/v2/kit/platform"
	errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/mock"
	influxdbtesting "github.com/influxdata/influxdb/v2/testing"
	"github.com/stretchr/testify/assert"
	"go.uber.org/zap/zaptest"
)

func initHttpService(t *testing.T) (influxdb.DBRPMappingService, *httptest.Server, func()) {
	t.Helper()
	ctx := context.Background()
	bucketSvc := mock.NewBucketService()
	orgSvc := &mock.OrganizationService{
		FindOrganizationF: func(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
			if filter.Name == nil || *filter.Name != "org" {
				return nil, errors.New("not found")
			}
			return &influxdb.Organization{
				Name: "org",
				ID:   influxdbtesting.MustIDBase16("059af7ed2a034000"),
			}, nil
		},
	}

	s, closeS := influxdbtesting.NewTestBoltStore(t)
	svc := dbrp.NewService(ctx, bucketSvc, s)

	server := httptest.NewServer(dbrp.NewHTTPHandler(zaptest.NewLogger(t), svc, orgSvc))
	return svc, server, func() {
		closeS()
		server.Close()
	}
}

func Test_handlePostDBRP(t *testing.T) {
	table := []struct {
		Name         string
		ExpectedErr  error
		ExpectedDBRP *influxdb.DBRPMapping
		Input        io.Reader
	}{
		{
			Name: "Create valid dbrp",
			Input: strings.NewReader(`{
	"bucketID": "5555f7ed2a035555",
	"orgID": "059af7ed2a034000",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedDBRP: &influxdb.DBRPMapping{
				OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
			},
		},
		{
			Name: "Create valid dbrp by org name",
			Input: strings.NewReader(`{
	"bucketID": "5555f7ed2a035555",
	"org": "org",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedDBRP: &influxdb.DBRPMapping{
				OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
			},
		},
		{
			Name: "Create with no orgID or org",
			Input: strings.NewReader(`{
	"bucketID": "5555f7ed2a035555",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedErr: dbrp.ErrNoOrgProvided,
		},
		{
			Name: "Create with invalid orgID",
			Input: strings.NewReader(`{
	"bucketID": "5555f7ed2a035555",
	"orgID": "invalid",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedErr: dbrp.ErrInvalidOrgID("invalid", platform.ErrInvalidIDLength),
		},
		{
			Name: "Create with invalid org name",
			Input: strings.NewReader(`{
	"bucketID": "5555f7ed2a035555",
	"org": "invalid",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedErr: dbrp.ErrOrgNotFound("invalid"),
		},
		{
			Name: "Create with invalid bucket ID",
			Input: strings.NewReader(`{
	"bucketID": "invalid",
	"org": "org",
	"database": "mydb",
	"retention_policy": "autogen",
	"default": false
}`),
			ExpectedErr: dbrp.ErrInvalidBucketID("invalid", platform.ErrInvalidIDLength),
		},
	}

	for _, tt := range table {
		t.Run(tt.Name, func(t *testing.T) {
			if tt.ExpectedErr != nil && tt.ExpectedDBRP != nil {
				t.Fatal("one of `ExpectedErr` or `ExpectedDBRP` has to be set")
			}
			_, server, shutdown := initHttpService(t)
			defer shutdown()
			client := server.Client()

			resp, err := client.Post(server.URL+"/", "application/json", tt.Input)
			if err != nil {
				t.Fatal(err)
			}
			defer resp.Body.Close()

			if tt.ExpectedErr != nil {
				b, err := io.ReadAll(resp.Body)
				if err != nil {
					t.Fatal(err)
				}
				var actualErr errors2.Error
				if err := json.Unmarshal(b, &actualErr); err != nil {
					t.Fatal(err)
				}

				assert.Equal(t, tt.ExpectedErr.Error(), actualErr.Error())
				return
			}
			dbrp := &influxdb.DBRPMapping{}
			if err := json.NewDecoder(resp.Body).Decode(&dbrp); err != nil {
				t.Fatal(err)
			}

			if !dbrp.ID.Valid() {
				t.Fatalf("expected valid id, got an invalid one %s", dbrp.ID.String())
			}

			if dbrp.OrganizationID != tt.ExpectedDBRP.OrganizationID {
				t.Fatalf("expected orgid %s got %s", tt.ExpectedDBRP.OrganizationID, dbrp.OrganizationID)
			}

			if !dbrp.Default {
				t.Fatalf("expected dbrp to be marked as default")
			}
		})
	}
}

func Test_handleGetDBRPs(t *testing.T) {
	table := []struct {
		Name          string
		QueryParams   string
		ExpectedErr   error
		ExpectedDBRPs []influxdb.DBRPMapping
	}{
		{
			Name:        "ok",
			QueryParams: "orgID=059af7ed2a034000",
			ExpectedDBRPs: []influxdb.DBRPMapping{
				{
					ID:              influxdbtesting.MustIDBase16("1111111111111111"),
					BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
					OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
					Database:        "mydb",
					RetentionPolicy: "autogen",
					Default:         true,
				},
			},
		},
		{
			Name:        "invalid org",
			QueryParams: "orgID=invalid",
			ExpectedErr: dbrp.ErrInvalidOrgID("invalid", platform.ErrInvalidIDLength),
		},
		{
			Name:        "invalid bucket",
			QueryParams: "orgID=059af7ed2a034000&bucketID=invalid",
			ExpectedErr: dbrp.ErrInvalidBucketID("invalid", platform.ErrInvalidIDLength),
		},
		{
			Name:        "invalid default",
			QueryParams: "orgID=059af7ed2a034000&default=notabool",
			ExpectedErr: &errors2.Error{
				Code: errors2.EInvalid,
				Msg:  "invalid default parameter",
			},
		},
		{
			Name:        "no org",
			QueryParams: "default=true&retention_police=lol",
			ExpectedErr: dbrp.ErrNoOrgProvided,
		},
		{
			Name:          "no match",
			QueryParams:   "orgID=059af7ed2a034000&default=false",
			ExpectedDBRPs: []influxdb.DBRPMapping{},
		},
		{
			Name:        "all match",
			QueryParams: "orgID=059af7ed2a034000&default=true&rp=autogen&db=mydb&bucketID=5555f7ed2a035555&id=1111111111111111",
			ExpectedDBRPs: []influxdb.DBRPMapping{
				{
					ID:              influxdbtesting.MustIDBase16("1111111111111111"),
					BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
					OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
					Database:        "mydb",
					RetentionPolicy: "autogen",
					Default:         true,
				},
			},
		},
		{
			Name:        "org name",
			QueryParams: "org=org",
			ExpectedDBRPs: []influxdb.DBRPMapping{
				{
					ID:              influxdbtesting.MustIDBase16("1111111111111111"),
					BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
					OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
					Database:        "mydb",
					RetentionPolicy: "autogen",
					Default:         true,
				},
			},
		},
	}

	ctx := context.Background()
	for _, tt := range table {
		t.Run(tt.Name, func(t *testing.T) {
			if tt.ExpectedErr != nil && len(tt.ExpectedDBRPs) != 0 {
				t.Error("one of those has to be set")
			}
			svc, server, shutdown := initHttpService(t)
			defer shutdown()

			if svc, ok := svc.(*dbrp.Service); ok {
				svc.IDGen = mock.NewIDGenerator("1111111111111111", t)
			}
			db := &influxdb.DBRPMapping{
				BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
				OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
				Database:        "mydb",
				RetentionPolicy: "autogen",
				Default:         true,
			}
			if err := svc.Create(ctx, db); err != nil {
				t.Fatal(err)
			}

			client := server.Client()
			resp, err := client.Get(server.URL + "?" + tt.QueryParams)
			if err != nil {
				t.Fatal(err)
			}
			defer resp.Body.Close()

			if tt.ExpectedErr != nil {
				b, err := io.ReadAll(resp.Body)
				if err != nil {
					t.Fatal(err)
				}
				var actualErr errors2.Error
				if err := json.Unmarshal(b, &actualErr); err != nil {
					t.Fatal(err)
				}

				assert.Equal(t, tt.ExpectedErr.Error(), actualErr.Error())
				return
			}
			dbrps := struct {
				Content []influxdb.DBRPMapping `json:"content"`
			}{}
			if err := json.NewDecoder(resp.Body).Decode(&dbrps); err != nil {
				t.Fatal(err)
			}

			if len(dbrps.Content) != len(tt.ExpectedDBRPs) {
				t.Fatalf("expected %d dbrps got %d", len(tt.ExpectedDBRPs), len(dbrps.Content))
			}

			if !cmp.Equal(tt.ExpectedDBRPs, dbrps.Content) {
				t.Fatalf(cmp.Diff(tt.ExpectedDBRPs, dbrps.Content))
			}

		})
	}
}

func Test_handlePatchDBRP(t *testing.T) {
	table := []struct {
		Name         string
		ExpectedErr  error
		ExpectedDBRP *influxdb.DBRPMapping
		URLSuffix    string
		Input        io.Reader
	}{
		{
			Name:      "happy path update",
			URLSuffix: "/1111111111111111?orgID=059af7ed2a034000",
			Input: strings.NewReader(`{
	"retention_policy": "updaterp",
	"database": "wont_change"
}`),
			ExpectedDBRP: &influxdb.DBRPMapping{
				ID:              influxdbtesting.MustIDBase16("1111111111111111"),
				BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
				OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
				Database:        "mydb",
				RetentionPolicy: "updaterp",
				Default:         true,
			},
		},
		{
			Name:      "happy path update by org name",
			URLSuffix: "/1111111111111111?org=org",
			Input: strings.NewReader(`{
	"retention_policy": "updaterp",
	"database": "wont_change"
}`),
			ExpectedDBRP: &influxdb.DBRPMapping{
				ID:              influxdbtesting.MustIDBase16("1111111111111111"),
				BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
				OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
				Database:        "mydb",
				RetentionPolicy: "updaterp",
				Default:         true,
			},
		},
		{
			Name:      "invalid org",
			URLSuffix: "/1111111111111111?orgID=invalid",
			Input: strings.NewReader(`{
	"database": "updatedb"
}`),
			ExpectedErr: dbrp.ErrInvalidOrgID("invalid", platform.ErrInvalidIDLength),
		},
		{
			Name:      "no org",
			URLSuffix: "/1111111111111111",
			Input: strings.NewReader(`{
	"database": "updatedb"
}`),
			ExpectedErr: dbrp.ErrNoOrgProvided,
		},
		{
			Name:        "not found",
			URLSuffix:   "/1111111111111111?orgID=059af7ed2a034001",
			ExpectedErr: dbrp.ErrDBRPNotFound,
		},
	}

	ctx := context.Background()

	for _, tt := range table {
		t.Run(tt.Name, func(t *testing.T) {
			if tt.ExpectedErr != nil && tt.ExpectedDBRP != nil {
				t.Error("one of those has to be set")
			}
			svc, server, shutdown := initHttpService(t)
			defer shutdown()
			client := server.Client()

			if svc, ok := svc.(*dbrp.Service); ok {
				svc.IDGen = mock.NewIDGenerator("1111111111111111", t)
			}

			dbrp := &influxdb.DBRPMapping{
				BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
				OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
				Database:        "mydb",
				RetentionPolicy: "autogen",
				Default:         true,
			}
			if err := svc.Create(ctx, dbrp); err != nil {
				t.Fatal(err)
			}

			req, _ := http.NewRequest(http.MethodPatch, server.URL+tt.URLSuffix, tt.Input)
			resp, err := client.Do(req)
			if err != nil {
				t.Fatal(err)
			}
			defer resp.Body.Close()

			if tt.ExpectedErr != nil {
				b, err := io.ReadAll(resp.Body)
				if err != nil {
					t.Fatal(err)
				}
				var actualErr errors2.Error
				if err := json.Unmarshal(b, &actualErr); err != nil {
					t.Fatal(err)
				}

				assert.Equal(t, tt.ExpectedErr.Error(), actualErr.Error())
				return
			}
			dbrpResponse := struct {
				Content *influxdb.DBRPMapping `json:"content"`
			}{}

			if err := json.NewDecoder(resp.Body).Decode(&dbrpResponse); err != nil {
				t.Fatal(err)
			}

			if !cmp.Equal(tt.ExpectedDBRP, dbrpResponse.Content) {
				t.Fatalf(cmp.Diff(tt.ExpectedDBRP, dbrpResponse.Content))
			}
		})
	}
}

func Test_handleDeleteDBRP(t *testing.T) {
	table := []struct {
		Name              string
		URLSuffix         string
		ExpectedErr       error
		ExpectStillExists bool
	}{
		{
			Name:      "delete",
			URLSuffix: "/1111111111111111?orgID=059af7ed2a034000",
		},
		{
			Name:      "delete by org name",
			URLSuffix: "/1111111111111111?org=org",
		},
		{
			Name:        "invalid org",
			URLSuffix:   "/1111111111111111?orgID=invalid",
			ExpectedErr: dbrp.ErrInvalidOrgID("invalid", platform.ErrInvalidIDLength),
		},
		{
			Name:        "invalid org name",
			URLSuffix:   "/1111111111111111?org=invalid",
			ExpectedErr: dbrp.ErrOrgNotFound("invalid"),
		},
		{
			Name:        "no org",
			URLSuffix:   "/1111111111111111",
			ExpectedErr: dbrp.ErrNoOrgProvided,
		},
		{
			// Not found is not an error for Delete.
			Name:              "not found",
			URLSuffix:         "/1111111111111111?orgID=059af7ed2a034001",
			ExpectStillExists: true,
		},
	}

	ctx := context.Background()

	for _, tt := range table {
		t.Run(tt.Name, func(t *testing.T) {
			svc, server, shutdown := initHttpService(t)
			defer shutdown()
			client := server.Client()

			d := &influxdb.DBRPMapping{
				ID:              influxdbtesting.MustIDBase16("1111111111111111"),
				BucketID:        influxdbtesting.MustIDBase16("5555f7ed2a035555"),
				OrganizationID:  influxdbtesting.MustIDBase16("059af7ed2a034000"),
				Database:        "mydb",
				RetentionPolicy: "testrp",
				Default:         true,
			}
			if err := svc.Create(ctx, d); err != nil {
				t.Fatal(err)
			}

			req, _ := http.NewRequest(http.MethodDelete, server.URL+tt.URLSuffix, nil)
			resp, err := client.Do(req)
			if err != nil {
				t.Fatal(err)
			}
			defer resp.Body.Close()

			if tt.ExpectedErr != nil {
				b, err := io.ReadAll(resp.Body)
				if err != nil {
					t.Fatal(err)
				}
				var actualErr errors2.Error
				if err := json.Unmarshal(b, &actualErr); err != nil {
					t.Fatal(err)
				}

				assert.Equal(t, tt.ExpectedErr.Error(), actualErr.Error())
				return
			}

			if resp.StatusCode != http.StatusNoContent {
				t.Fatalf("expected status code %d, got %d", http.StatusNoContent, resp.StatusCode)
			}

			gotDBRP, err := svc.FindByID(ctx, influxdbtesting.MustIDBase16("059af7ed2a034000"), influxdbtesting.MustIDBase16("1111111111111111"))
			if tt.ExpectStillExists {
				if err != nil {
					t.Fatal(err)
				}
				if diff := cmp.Diff(d, gotDBRP); diff != "" {
					t.Fatal(diff)
				}
			} else {
				if err == nil {
					t.Fatal("expected error got none")
				}
				if !errors.Is(err, dbrp.ErrDBRPNotFound) {
					t.Fatalf("expected err dbrp not found, got %s", err)
				}
			}
		})
	}
}
