package transport

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"
	"time"

	"github.com/go-chi/chi"
	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/dashboards"
	dashboardstesting "github.com/influxdata/influxdb/v2/dashboards/testing"
	ihttp "github.com/influxdata/influxdb/v2/http"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/kv"
	"github.com/influxdata/influxdb/v2/label"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/influxdata/influxdb/v2/tenant"
	itesting "github.com/influxdata/influxdb/v2/testing"
	"github.com/yudai/gojsondiff"
	"github.com/yudai/gojsondiff/formatter"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
)

func newDashboardHandler(log *zap.Logger, opts ...option) *DashboardHandler {
	deps := dashboardDependencies{
		dashboardService: mock.NewDashboardService(),
		userService:      mock.NewUserService(),
		orgService:       mock.NewOrganizationService(),
		labelService:     mock.NewLabelService(),
		urmService:       mock.NewUserResourceMappingService(),
	}

	for _, opt := range opts {
		opt(&deps)
	}

	return NewDashboardHandler(
		log,
		deps.dashboardService,
		deps.labelService,
		deps.userService,
		deps.orgService,
		tenant.NewURMHandler(
			log.With(zap.String("handler", "urm")),
			influxdb.DashboardsResourceType,
			"id",
			deps.userService,
			deps.urmService,
		),
		label.NewHTTPEmbeddedHandler(
			log.With(zap.String("handler", "label")),
			influxdb.DashboardsResourceType,
			deps.labelService,
		),
	)
}

func TestService_handleGetDashboards(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
		LabelService     influxdb.LabelService
	}
	type args struct {
		queryParams map[string][]string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "get all dashboards",
			fields: fields{
				&mock.DashboardService{
					FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) {
						return []*influxdb.Dashboard{
							{
								ID:             dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
								OrganizationID: 1,
								Name:           "hello",
								Description:    "oh hello there!",
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2009, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							},
							{
								ID:             dashboardstesting.MustIDBase16("0ca2204eca2204e0"),
								OrganizationID: 1,
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Name: "example",
							},
						}, 2, nil
					},
				},
				&mock.LabelService{
					FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
						labels := []*influxdb.Label{
							{
								ID:   dashboardstesting.MustIDBase16("fc3dc670a4be9b9a"),
								Name: "label",
								Properties: map[string]string{
									"color": "fff000",
								},
							},
						}
						return labels, nil
					},
				},
			},
			args: args{},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "links": {
    "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0"
  },
  "dashboards": [
    {
      "id": "da7aba5e5d81e550",
      "orgID": "0000000000000001",
      "name": "hello",
      "description": "oh hello there!",
      "labels": [
        {
          "id": "fc3dc670a4be9b9a",
          "name": "label",
          "properties": {
            "color": "fff000"
          }
        }
      ],
      "meta": {
        "createdAt": "2009-11-10T23:00:00Z",
        "updatedAt": "2009-11-11T00:00:00Z"
      },
      "cells": [
        {
          "id": "da7aba5e5d81e550",
          "x": 1,
          "y": 2,
          "w": 3,
          "h": 4,
          "links": {
            "self": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550",
            "view": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550/view"
          }
        }
      ],
      "links": {
        "self": "/api/v2/dashboards/da7aba5e5d81e550",
        "org": "/api/v2/orgs/0000000000000001",
        "members": "/api/v2/dashboards/da7aba5e5d81e550/members",
        "owners": "/api/v2/dashboards/da7aba5e5d81e550/owners",
        "cells": "/api/v2/dashboards/da7aba5e5d81e550/cells",
        "labels": "/api/v2/dashboards/da7aba5e5d81e550/labels"
      }
    },
    {
      "id": "0ca2204eca2204e0",
      "orgID": "0000000000000001",
      "name": "example",
      "description": "",
			"labels": [
        {
          "id": "fc3dc670a4be9b9a",
          "name": "label",
          "properties": {
            "color": "fff000"
          }
        }
      ],
      "meta": {
        "createdAt": "2012-11-10T23:00:00Z",
        "updatedAt": "2012-11-11T00:00:00Z"
      },
      "cells": [],
      "links": {
        "self": "/api/v2/dashboards/0ca2204eca2204e0",
        "org": "/api/v2/orgs/0000000000000001",
        "members": "/api/v2/dashboards/0ca2204eca2204e0/members",
        "owners": "/api/v2/dashboards/0ca2204eca2204e0/owners",
        "cells": "/api/v2/dashboards/0ca2204eca2204e0/cells",
        "labels": "/api/v2/dashboards/0ca2204eca2204e0/labels"
      }
    }
  ]
}
`,
			},
		},
		{
			name: "get all dashboards when there are none",
			fields: fields{
				&mock.DashboardService{
					FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) {
						return []*influxdb.Dashboard{}, 0, nil
					},
				},
				&mock.LabelService{
					FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
						return []*influxdb.Label{}, nil
					},
				},
			},
			args: args{},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "links": {
    "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0"
  },
  "dashboards": []
}`,
			},
		},
		{
			name: "get all dashboards belonging to org 1",
			fields: fields{
				&mock.DashboardService{
					FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) {
						return []*influxdb.Dashboard{
							{
								ID:             dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
								OrganizationID: 1,
								Name:           "hello",
								Description:    "oh hello there!",
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2009, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							},
						}, 1, nil
					},
				},
				&mock.LabelService{
					FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
						labels := []*influxdb.Label{
							{
								ID:   dashboardstesting.MustIDBase16("fc3dc670a4be9b9a"),
								Name: "label",
								Properties: map[string]string{
									"color": "fff000",
								},
							},
						}
						return labels, nil
					},
				},
			},
			args: args{
				map[string][]string{
					"orgID": {"0000000000000001"},
				},
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "links": {
    "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0&orgID=0000000000000001"
  },
  "dashboards": [
    {
      "id": "da7aba5e5d81e550",
      "orgID": "0000000000000001",
      "name": "hello",
      "description": "oh hello there!",
      "meta": {
        "createdAt": "2009-11-10T23:00:00Z",
        "updatedAt": "2009-11-11T00:00:00Z"
    },
    "labels": [
      {
        "id": "fc3dc670a4be9b9a",
        "name": "label",
        "properties": {
          "color": "fff000"
        }
      }
    ],
      "cells": [
        {
          "id": "da7aba5e5d81e550",
          "x": 1,
          "y": 2,
          "w": 3,
          "h": 4,
          "links": {
            "self": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550",
            "view": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550/view"
          }
        }
      ],
      "links": {
        "self": "/api/v2/dashboards/da7aba5e5d81e550",
        "org": "/api/v2/orgs/0000000000000001",
        "members": "/api/v2/dashboards/da7aba5e5d81e550/members",
        "owners": "/api/v2/dashboards/da7aba5e5d81e550/owners",
        "cells": "/api/v2/dashboards/da7aba5e5d81e550/cells",
        "labels": "/api/v2/dashboards/da7aba5e5d81e550/labels"
      }
    }
  ]
}
`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			log := zaptest.NewLogger(t)
			h := newDashboardHandler(
				log,
				withDashboardService(tt.fields.DashboardService),
				withLabelService(tt.fields.LabelService),
			)

			r := httptest.NewRequest("GET", "http://any.url", nil)

			qp := r.URL.Query()
			for k, vs := range tt.args.queryParams {
				for _, v := range vs {
					qp.Add(k, v)
				}
			}
			r.URL.RawQuery = qp.Encode()

			w := httptest.NewRecorder()

			h.handleGetDashboards(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handleGetDashboards() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handleGetDashboards() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
				t.Errorf("%q, handleGetDashboards(). error unmarshalling json %v", tt.name, err)
			} else if tt.wants.body != "" && !eq {
				t.Errorf("%q. handleGetDashboards() = ***%s***", tt.name, diff)
			}
		})
	}
}

func TestService_handleGetDashboard(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id          string
		queryString map[string]string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "get a dashboard by id with view properties",
			fields: fields{
				&mock.DashboardService{
					GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) {
						return &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}}, nil
					},
					FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							return &influxdb.Dashboard{
								ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
								OrganizationID: 1,
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Name: "hello",
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
										View: &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}},
									},
								},
							}, nil
						}

						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
				queryString: map[string]string{
					"include": "properties",
				},
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "id": "020f755c3c082000",
  "orgID": "0000000000000001",
  "name": "hello",
  "description": "",
  "labels": [],
  "meta": {
    "createdAt": "2012-11-10T23:00:00Z",
    "updatedAt": "2012-11-11T00:00:00Z"
  },
  "cells": [
    {
      "id": "da7aba5e5d81e550",
      "x": 1,
      "y": 2,
      "w": 3,
			"h": 4,
			"name": "the cell name",
			"properties": {
			"shape": "chronograf-v2",
			"axes": null,
			"colors": null,
			"geom": "",
			"staticLegend": {},
			"position": "",
			"note": "",
			"queries": null,
			"shadeBelow": false,
			"hoverDimension": "",
			"showNoteWhenEmpty": false,
			"timeFormat": "",
			"type": "xy",
			"xColumn": "",
			"generateXAxisTicks": null,
			"xTotalTicks": 0,
			"xTickStart": 0,
			"xTickStep": 0,
			"yColumn": "",
			"generateYAxisTicks": null,
			"yTotalTicks": 0,
			"yTickStart": 0,
			"yTickStep": 0,
			"legendColorizeRows": false,
			"legendHide": false,
			"legendOpacity": 0,
			"legendOrientationThreshold": 0
	  },
	  "links": {
		"self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
		"view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
	  }
    }
  ],
  "links": {
	     "self": "/api/v2/dashboards/020f755c3c082000",
	     "org": "/api/v2/orgs/0000000000000001",
	     "members": "/api/v2/dashboards/020f755c3c082000/members",
	     "owners": "/api/v2/dashboards/020f755c3c082000/owners",
	     "cells": "/api/v2/dashboards/020f755c3c082000/cells",
	     "labels": "/api/v2/dashboards/020f755c3c082000/labels"
	}
}
`,
			},
		},
		{
			name: "get a dashboard by id with view properties, but a cell doesnt exist",
			fields: fields{
				&mock.DashboardService{
					GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) {
						return nil, nil
					},
					FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							return &influxdb.Dashboard{
								ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
								OrganizationID: 1,
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Name: "hello",
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							}, nil
						}

						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
				queryString: map[string]string{
					"include": "properties",
				},
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "id": "020f755c3c082000",
  "orgID": "0000000000000001",
  "name": "hello",
  "description": "",
  "labels": [],
  "meta": {
    "createdAt": "2012-11-10T23:00:00Z",
    "updatedAt": "2012-11-11T00:00:00Z"
  },
  "cells": [
    {
      "id": "da7aba5e5d81e550",
      "x": 1,
      "y": 2,
      "w": 3,
	  "h": 4,
	  "links": {
		"self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
		"view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
	  }
    }
  ],
  "links": {
	     "self": "/api/v2/dashboards/020f755c3c082000",
	     "org": "/api/v2/orgs/0000000000000001",
	     "members": "/api/v2/dashboards/020f755c3c082000/members",
	     "owners": "/api/v2/dashboards/020f755c3c082000/owners",
	     "cells": "/api/v2/dashboards/020f755c3c082000/cells",
	     "labels": "/api/v2/dashboards/020f755c3c082000/labels"
	}
}
`,
			},
		},
		{
			name: "get a dashboard by id doesnt return cell properties if they exist by default",
			fields: fields{
				&mock.DashboardService{
					GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) {
						return &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}}, nil
					},
					FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							return &influxdb.Dashboard{
								ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
								OrganizationID: 1,
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Name: "hello",
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							}, nil
						}

						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id:          "020f755c3c082000",
				queryString: map[string]string{},
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "id": "020f755c3c082000",
  "orgID": "0000000000000001",
  "name": "hello",
  "description": "",
  "labels": [],
  "meta": {
    "createdAt": "2012-11-10T23:00:00Z",
    "updatedAt": "2012-11-11T00:00:00Z"
  },
  "cells": [
    {
      "id": "da7aba5e5d81e550",
      "x": 1,
      "y": 2,
      "w": 3,
	  "h": 4,
	  "links": {
		"self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
		"view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
	  }
    }
  ],
  "links": {
	     "self": "/api/v2/dashboards/020f755c3c082000",
	     "org": "/api/v2/orgs/0000000000000001",
	     "members": "/api/v2/dashboards/020f755c3c082000/members",
	     "owners": "/api/v2/dashboards/020f755c3c082000/owners",
	     "cells": "/api/v2/dashboards/020f755c3c082000/cells",
	     "labels": "/api/v2/dashboards/020f755c3c082000/labels"
	}
}
`,
			},
		},
		{
			name: "get a dashboard by id",
			fields: fields{
				&mock.DashboardService{
					FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							return &influxdb.Dashboard{
								ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
								OrganizationID: 1,
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
								},
								Name: "hello",
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							}, nil
						}

						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
		{
		  "id": "020f755c3c082000",
		  "orgID": "0000000000000001",
		  "name": "hello",
		  "description": "",
		  "labels": [],
		  "meta": {
		    "createdAt": "2012-11-10T23:00:00Z",
		    "updatedAt": "2012-11-11T00:00:00Z"
		  },
		  "cells": [
		    {
			  "id": "da7aba5e5d81e550",
		      "x": 1,
		      "y": 2,
		      "w": 3,
		      "h": 4,
		      "links": {
		        "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
		        "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
		      }
		    }
		  ],
		  "links": {
		    "self": "/api/v2/dashboards/020f755c3c082000",
		    "org": "/api/v2/orgs/0000000000000001",
		    "members": "/api/v2/dashboards/020f755c3c082000/members",
		    "owners": "/api/v2/dashboards/020f755c3c082000/owners",
		    "cells": "/api/v2/dashboards/020f755c3c082000/cells",
		    "labels": "/api/v2/dashboards/020f755c3c082000/labels"
		  }
		}
		`,
			},
		},
		{
			name: "not found",
			fields: fields{
				&mock.DashboardService{
					FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						return nil, &errors.Error{
							Code: errors.ENotFound,
							Msg:  influxdb.ErrDashboardNotFound,
						}
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode: http.StatusNotFound,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			r := httptest.NewRequest("GET", "http://any.url", nil)

			urlQuery := r.URL.Query()

			for k, v := range tt.args.queryString {
				urlQuery.Add(k, v)
			}

			r.URL.RawQuery = urlQuery.Encode()

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)

			w := httptest.NewRecorder()

			h.handleGetDashboard(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)
			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handleGetDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handleGetDashboard() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if eq, diff, err := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && err != nil {
				t.Errorf("%q, handleGetDashboard(). error unmarshalling json %v", tt.name, err)
			} else if tt.wants.body != "" && !eq {
				t.Errorf("%q. handleGetDashboard() = ***%s***", tt.name, diff)
			}
		})
	}
}

func TestService_handlePostDashboard(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		dashboard *influxdb.Dashboard
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "create a new dashboard",
			fields: fields{
				&mock.DashboardService{
					CreateDashboardF: func(ctx context.Context, c *influxdb.Dashboard) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						c.Meta = influxdb.DashboardMeta{
							CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
							UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
						}
						return nil
					},
				},
			},
			args: args{
				dashboard: &influxdb.Dashboard{
					ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
					OrganizationID: 1,
					Name:           "hello",
					Description:    "howdy there",
					Cells: []*influxdb.Cell{
						{
							ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
							CellProperty: influxdb.CellProperty{
								X: 1,
								Y: 2,
								W: 3,
								H: 4,
							},
						},
					},
				},
			},
			wants: wants{
				statusCode:  http.StatusCreated,
				contentType: "application/json; charset=utf-8",
				body: `
						{
						"id": "020f755c3c082000",
						"orgID": "0000000000000001",
						"name": "hello",
						"description": "howdy there",
						"labels": [],
						"meta": {
							"createdAt": "2012-11-10T23:00:00Z",
							"updatedAt": "2012-11-11T00:00:00Z"
						},
						"cells": [
							{
								"id": "da7aba5e5d81e550",
								"x": 1,
								"y": 2,
								"w": 3,
								"h": 4,
								"links": {
									"self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
									"view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
								}
							}
						],
						"links": {
							"self": "/api/v2/dashboards/020f755c3c082000",
							"org": "/api/v2/orgs/0000000000000001",
							"members": "/api/v2/dashboards/020f755c3c082000/members",
							"owners": "/api/v2/dashboards/020f755c3c082000/owners",
							"cells": "/api/v2/dashboards/020f755c3c082000/cells",
							"labels": "/api/v2/dashboards/020f755c3c082000/labels"
						}
						}`,
			},
		},
		{
			name: "create a new dashboard with cell view properties",
			fields: fields{
				&mock.DashboardService{
					CreateDashboardF: func(ctx context.Context, c *influxdb.Dashboard) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						c.Meta = influxdb.DashboardMeta{
							CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
							UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC),
						}
						c.Cells = []*influxdb.Cell{
							{
								ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
								CellProperty: influxdb.CellProperty{
									X: 1,
									Y: 2,
									W: 3,
									H: 4,
								},
								View: &influxdb.View{
									ViewContents: influxdb.ViewContents{
										Name: "hello a view",
									},
									Properties: influxdb.XYViewProperties{
										Type: influxdb.ViewPropertyTypeXY,
										Note: "note",
									},
								},
							},
						}
						return nil
					},
				},
			},
			args: args{
				dashboard: &influxdb.Dashboard{
					ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
					OrganizationID: 1,
					Name:           "hello",
					Description:    "howdy there",
					Cells: []*influxdb.Cell{
						{
							ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
							CellProperty: influxdb.CellProperty{
								X: 1,
								Y: 2,
								W: 3,
								H: 4,
							},
							View: &influxdb.View{
								ViewContents: influxdb.ViewContents{
									Name: "hello a view",
								},
								Properties: struct {
									influxdb.XYViewProperties
									Shape string
								}{
									XYViewProperties: influxdb.XYViewProperties{
										Note: "note",
										Type: influxdb.ViewPropertyTypeXY,
									},
									Shape: "chronograf-v2",
								},
							},
						},
					},
				},
			},
			wants: wants{
				statusCode:  http.StatusCreated,
				contentType: "application/json; charset=utf-8",
				body: `
				{
					"id": "020f755c3c082000",
					"orgID": "0000000000000001",
					"name": "hello",
					"description": "howdy there",
					"labels": [],
					"meta": {
						"createdAt": "2012-11-10T23:00:00Z",
						"updatedAt": "2012-11-11T00:00:00Z"
					},
					"cells": [
						{
							"id": "da7aba5e5d81e550",
							"x": 1,
							"y": 2,
							"w": 3,
							"h": 4,
							"name": "hello a view",
							"properties": {
								"shape": "chronograf-v2",
								"axes": null,
								"colors": null,
								"geom": "",
								"staticLegend": {},
								"note": "note",
								"position": "",
								"queries": null,
								"shadeBelow": false,
								"hoverDimension": "",
								"showNoteWhenEmpty": false,
								"timeFormat": "",
								"type": "",
								"xColumn": "",
								"generateXAxisTicks": null,
								"xTotalTicks": 0,
								"xTickStart": 0,
								"xTickStep": 0,
								"yColumn": "",
								"generateYAxisTicks": null,
								"yTotalTicks": 0,
								"yTickStart": 0,
								"yTickStep": 0,
								"type": "xy",
								"legendColorizeRows": false,
								"legendHide": false,
								"legendOpacity": 0,
								"legendOrientationThreshold": 0
							},
							"links": {
								"self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
								"view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
							}
						}
					],
					"links": {
						"self": "/api/v2/dashboards/020f755c3c082000",
						"org": "/api/v2/orgs/0000000000000001",
						"members": "/api/v2/dashboards/020f755c3c082000/members",
						"owners": "/api/v2/dashboards/020f755c3c082000/owners",
						"cells": "/api/v2/dashboards/020f755c3c082000/cells",
						"labels": "/api/v2/dashboards/020f755c3c082000/labels"
					}
				}`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			b, err := json.Marshal(tt.args.dashboard)
			if err != nil {
				t.Fatalf("failed to unmarshal dashboard: %v", err)
			}

			r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
			w := httptest.NewRecorder()

			h.handlePostDashboard(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handlePostDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handlePostDashboard() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
				t.Errorf("%q, handlePostDashboard(). error unmarshalling json %v", tt.name, err)
			} else if tt.wants.body != "" && !eq {
				t.Errorf("%q. handlePostDashboard() = ***%s***", tt.name, diff)
			}
		})
	}
}

func TestService_handleDeleteDashboard(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "remove a dashboard by id",
			fields: fields{
				&mock.DashboardService{
					DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							return nil
						}

						return fmt.Errorf("wrong id")
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode: http.StatusNoContent,
			},
		},
		{
			name: "dashboard not found",
			fields: fields{
				&mock.DashboardService{
					DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
						return &errors.Error{
							Code: errors.ENotFound,
							Msg:  influxdb.ErrDashboardNotFound,
						}
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode: http.StatusNotFound,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			r := httptest.NewRequest("GET", "http://any.url", nil)

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)
			w := httptest.NewRecorder()

			h.handleDeleteDashboard(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handleDeleteDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handleDeleteDashboard() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if tt.wants.body != "" {
				if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
					t.Errorf("%q, handleDeleteDashboard(). error unmarshalling json %v", tt.name, err)
				} else if !eq {
					t.Errorf("%q. handleDeleteDashboard() = ***%s***", tt.name, diff)
				}
			}
		})
	}
}

func TestService_handlePatchDashboard(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id   string
		name string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "update a dashboard name",
			fields: fields{
				&mock.DashboardService{
					UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) {
						if id == dashboardstesting.MustIDBase16("020f755c3c082000") {
							d := &influxdb.Dashboard{
								ID:             dashboardstesting.MustIDBase16("020f755c3c082000"),
								OrganizationID: 1,
								Name:           "hello",
								Meta: influxdb.DashboardMeta{
									CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC),
									UpdatedAt: time.Date(2012, time.November, 10, 25, 0, 0, 0, time.UTC),
								},
								Cells: []*influxdb.Cell{
									{
										ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"),
										CellProperty: influxdb.CellProperty{
											X: 1,
											Y: 2,
											W: 3,
											H: 4,
										},
									},
								},
							}

							if upd.Name != nil {
								d.Name = *upd.Name
							}

							return d, nil
						}

						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id:   "020f755c3c082000",
				name: "example",
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
		{
		  "id": "020f755c3c082000",
		  "orgID": "0000000000000001",
		  "name": "example",
		  "description": "",
		  "labels": [],
		  "meta": {
		    "createdAt": "2012-11-10T23:00:00Z",
		    "updatedAt": "2012-11-11T01:00:00Z"
		  },
		  "cells": [
		    {
		      "id": "da7aba5e5d81e550",
		      "x": 1,
		      "y": 2,
		      "w": 3,
		      "h": 4,
		      "links": {
		        "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550",
		        "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view"
		      }
		    }
		  ],
		  "links": {
		    "self": "/api/v2/dashboards/020f755c3c082000",
		    "org": "/api/v2/orgs/0000000000000001",
		    "members": "/api/v2/dashboards/020f755c3c082000/members",
		    "owners": "/api/v2/dashboards/020f755c3c082000/owners",
		    "cells": "/api/v2/dashboards/020f755c3c082000/cells",
		    "labels": "/api/v2/dashboards/020f755c3c082000/labels"
		  }
		}
		`,
			},
		},
		{
			name: "update a dashboard with empty request body",
			fields: fields{
				&mock.DashboardService{
					UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) {
						return nil, fmt.Errorf("not found")
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode: http.StatusBadRequest,
			},
		},
		{
			name: "dashboard not found",
			fields: fields{
				&mock.DashboardService{
					UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) {
						return nil, &errors.Error{
							Code: errors.ENotFound,
							Msg:  influxdb.ErrDashboardNotFound,
						}
					},
				},
			},
			args: args{
				id:   "020f755c3c082000",
				name: "hello",
			},
			wants: wants{
				statusCode: http.StatusNotFound,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			upd := influxdb.DashboardUpdate{}
			if tt.args.name != "" {
				upd.Name = &tt.args.name
			}

			b, err := json.Marshal(upd)
			if err != nil {
				t.Fatalf("failed to unmarshal dashboard update: %v", err)
			}

			r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)

			w := httptest.NewRecorder()

			h.handlePatchDashboard(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handlePatchDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handlePatchDashboard() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if tt.wants.body != "" {
				if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
					t.Errorf("%q, handlePatchDashboard(). error unmarshalling json %v", tt.name, err)
				} else if !eq {
					t.Errorf("%q. handlePatchDashboard() = ***%s***", tt.name, diff)
				}
			}
		})
	}
}

func TestService_handlePostDashboardCell(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id   string
		body string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "empty body",
			fields: fields{
				&mock.DashboardService{
					AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						return nil
					},
				},
			},
			args: args{
				id: "020f755c3c082000",
			},
			wants: wants{
				statusCode:  http.StatusBadRequest,
				contentType: "application/json; charset=utf-8",
				body:        `{"code":"invalid","message":"bad request json body: EOF"}`,
			},
		},
		{
			name: "no properties",
			fields: fields{
				&mock.DashboardService{
					AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						return nil
					},
				},
			},
			args: args{
				id:   "020f755c3c082000",
				body: `{"bad":1}`,
			},
			wants: wants{
				statusCode:  http.StatusBadRequest,
				contentType: "application/json; charset=utf-8",
				body: `
				{
					"code": "invalid",
					"message": "req body is empty"
				}`,
			},
		},
		{
			name: "bad dash id",
			fields: fields{
				&mock.DashboardService{
					AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						return nil
					},
				},
			},
			args: args{
				id:   "fff",
				body: `{}`,
			},
			wants: wants{
				statusCode:  http.StatusBadRequest,
				contentType: "application/json; charset=utf-8",
				body: `
				{
					"code": "invalid",
					"message": "id must have a length of 16 bytes"
				}`,
			},
		},
		{
			name: "general create a dashboard cell",
			fields: fields{
				&mock.DashboardService{
					AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error {
						c.ID = dashboardstesting.MustIDBase16("020f755c3c082000")
						return nil
					},
					GetDashboardCellViewF: func(ctx context.Context, id1, id2 platform.ID) (*influxdb.View, error) {
						return &influxdb.View{
							ViewContents: influxdb.ViewContents{
								ID: dashboardstesting.MustIDBase16("020f755c3c082001"),
							}}, nil
					},
				},
			},
			args: args{
				id:   "020f755c3c082000",
				body: `{"x":10,"y":11,"name":"name1","usingView":"020f755c3c082001"}`,
			},
			wants: wants{
				statusCode:  http.StatusCreated,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "id": "020f755c3c082000",
  "x": 10,
  "y": 11,
  "w": 0,
  "h": 0,
  "links": {
    "self": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000",
    "view": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000/view"
  }
}
`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			buf := new(bytes.Buffer)
			_, _ = buf.WriteString(tt.args.body)
			r := httptest.NewRequest("POST", "http://any.url", buf)

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)

			w := httptest.NewRecorder()

			h.handlePostDashboardCell(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handlePostDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handlePostDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if tt.wants.body != "" {
				if eq, diff, err := jsonEqual(tt.wants.body, string(body)); err != nil {
					t.Errorf("%q, handlePostDashboardCell(). error unmarshalling json %v", tt.name, err)
				} else if !eq {
					t.Errorf("%q. handlePostDashboardCell() = ***%s***", tt.name, diff)
				}
			}
		})
	}
}

func TestService_handleDeleteDashboardCell(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id     string
		cellID string
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "remove a dashboard cell",
			fields: fields{
				&mock.DashboardService{
					RemoveDashboardCellF: func(ctx context.Context, id platform.ID, cellID platform.ID) error {
						return nil
					},
				},
			},
			args: args{
				id:     "020f755c3c082000",
				cellID: "020f755c3c082000",
			},
			wants: wants{
				statusCode: http.StatusNoContent,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			r := httptest.NewRequest("GET", "http://any.url", nil)

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			rctx.URLParams.Add("cellID", tt.args.cellID)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)

			w := httptest.NewRecorder()

			h.handleDeleteDashboardCell(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handleDeleteDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handleDeleteDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if tt.wants.body != "" {
				if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
					t.Errorf("%q, handleDeleteDashboardCell(). error unmarshalling json %v", tt.name, err)
				} else if !eq {
					t.Errorf("%q. handleDeleteDashboardCell() = ***%s***", tt.name, diff)
				}
			}
		})
	}
}

func TestService_handlePatchDashboardCell(t *testing.T) {
	type fields struct {
		DashboardService influxdb.DashboardService
	}
	type args struct {
		id     string
		cellID string
		x      int32
		y      int32
		w      int32
		h      int32
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "update a dashboard cell",
			fields: fields{
				&mock.DashboardService{
					UpdateDashboardCellF: func(ctx context.Context, id, cellID platform.ID, upd influxdb.CellUpdate) (*influxdb.Cell, error) {
						cell := &influxdb.Cell{
							ID: dashboardstesting.MustIDBase16("020f755c3c082000"),
						}

						if err := upd.Apply(cell); err != nil {
							return nil, err
						}

						return cell, nil
					},
				},
			},
			args: args{
				id:     "020f755c3c082000",
				cellID: "020f755c3c082000",
				x:      10,
				y:      11,
			},
			wants: wants{
				statusCode:  http.StatusOK,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "id": "020f755c3c082000",
  "x": 10,
  "y": 11,
  "w": 0,
  "h": 0,
  "links": {
    "self": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000",
    "view": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000/view"
  }
}
`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withDashboardService(tt.fields.DashboardService),
			)

			upd := influxdb.CellUpdate{}
			if tt.args.x != 0 {
				upd.X = &tt.args.x
			}
			if tt.args.y != 0 {
				upd.Y = &tt.args.y
			}
			if tt.args.w != 0 {
				upd.W = &tt.args.w
			}
			if tt.args.h != 0 {
				upd.H = &tt.args.h
			}

			b, err := json.Marshal(upd)
			if err != nil {
				t.Fatalf("failed to unmarshal cell: %v", err)
			}

			r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))

			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tt.args.id)
			rctx.URLParams.Add("cellID", tt.args.cellID)
			r = r.WithContext(context.WithValue(
				context.Background(),
				chi.RouteCtxKey,
				rctx),
			)
			w := httptest.NewRecorder()

			h.handlePatchDashboardCell(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("%q. handlePatchDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("%q. handlePatchDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType)
			}
			if tt.wants.body != "" {
				if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
					t.Errorf("%q, handlePatchDashboardCell(). error unmarshalling json %v", tt.name, err)
				} else if !eq {
					t.Errorf("%q. handlePatchDashboardCell() = ***%s***", tt.name, diff)
				}
			}
		})
	}
}

func Test_dashboardCellIDPath(t *testing.T) {
	t.Parallel()
	dashboard, err := platform.IDFromString("deadbeefdeadbeef")
	if err != nil {
		t.Fatal(err)
	}

	cell, err := platform.IDFromString("cade9a7ecade9a7e")
	if err != nil {
		t.Fatal(err)
	}

	want := "/api/v2/dashboards/deadbeefdeadbeef/cells/cade9a7ecade9a7e"
	if got := dashboardCellIDPath(*dashboard, *cell); got != want {
		t.Errorf("dashboardCellIDPath() = got: %s want: %s", got, want)
	}
}

func initDashboardService(f dashboardstesting.DashboardFields, t *testing.T) (influxdb.DashboardService, string, func()) {
	t.Helper()
	log := zaptest.NewLogger(t)
	store := itesting.NewTestInmemStore(t)

	kvsvc := kv.NewService(log, store, &mock.OrganizationService{})
	kvsvc.IDGenerator = f.IDGenerator

	svc := dashboards.NewService(
		store,
		kvsvc, // operation log storage
	)

	svc.IDGenerator = f.IDGenerator

	ctx := context.Background()

	for _, d := range f.Dashboards {
		if err := svc.PutDashboard(ctx, d); err != nil {
			t.Fatalf("failed to populate dashboard")
		}
	}

	h := newDashboardHandler(
		log,
		withDashboardService(svc),
	)

	r := chi.NewRouter()
	r.Mount(h.Prefix(), h)
	server := httptest.NewServer(r)

	httpClient, err := ihttp.NewHTTPClient(server.URL, "", false)
	if err != nil {
		t.Fatal(err)
	}

	client := DashboardService{Client: httpClient}

	return &client, "", server.Close
}

func TestDashboardService(t *testing.T) {
	t.Parallel()
	dashboardstesting.DeleteDashboard(initDashboardService, t)
}

func TestService_handlePostDashboardLabel(t *testing.T) {
	type fields struct {
		LabelService influxdb.LabelService
	}
	type args struct {
		labelMapping *influxdb.LabelMapping
		dashboardID  platform.ID
	}
	type wants struct {
		statusCode  int
		contentType string
		body        string
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		wants  wants
	}{
		{
			name: "add label to dashboard",
			fields: fields{
				LabelService: &mock.LabelService{
					FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*influxdb.Label, error) {
						return &influxdb.Label{
							ID:   1,
							Name: "label",
							Properties: map[string]string{
								"color": "fff000",
							},
						}, nil
					},
					CreateLabelMappingFn: func(ctx context.Context, m *influxdb.LabelMapping) error { return nil },
				},
			},
			args: args{
				labelMapping: &influxdb.LabelMapping{
					ResourceID: 100,
					LabelID:    1,
				},
				dashboardID: 100,
			},
			wants: wants{
				statusCode:  http.StatusCreated,
				contentType: "application/json; charset=utf-8",
				body: `
{
  "label": {
    "id": "0000000000000001",
    "name": "label",
    "properties": {
      "color": "fff000"
    }
  },
  "links": {
    "self": "/api/v2/labels/0000000000000001"
  }
}
`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := newDashboardHandler(
				zaptest.NewLogger(t),
				withLabelService(tt.fields.LabelService),
				withDashboardService(&mock.DashboardService{
					FindDashboardByIDF: func(_ context.Context, id platform.ID) (*influxdb.Dashboard, error) {
						return &influxdb.Dashboard{
							ID:             id,
							OrganizationID: platform.ID(25),
						}, nil
					},
				}),
			)

			router := chi.NewRouter()
			router.Mount(h.Prefix(), h)

			b, err := json.Marshal(tt.args.labelMapping)
			if err != nil {
				t.Fatalf("failed to unmarshal label mapping: %v", err)
			}

			url := fmt.Sprintf("http://localhost:8086/api/v2/dashboards/%s/labels", tt.args.dashboardID)
			r := httptest.NewRequest("POST", url, bytes.NewReader(b))
			w := httptest.NewRecorder()

			router.ServeHTTP(w, r)

			res := w.Result()
			content := res.Header.Get("Content-Type")
			body, _ := io.ReadAll(res.Body)

			if res.StatusCode != tt.wants.statusCode {
				t.Errorf("got %v, want %v", res.StatusCode, tt.wants.statusCode)
			}
			if tt.wants.contentType != "" && content != tt.wants.contentType {
				t.Errorf("got %v, want %v", content, tt.wants.contentType)
			}
			if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
				t.Errorf("Diff\n%s", diff)
			}
		})
	}
}

func jsonEqual(s1, s2 string) (eq bool, diff string, err error) {
	if s1 == s2 {
		return true, "", nil
	}

	if s1 == "" {
		return false, s2, fmt.Errorf("s1 is empty")
	}

	if s2 == "" {
		return false, s1, fmt.Errorf("s2 is empty")
	}

	var o1 interface{}
	if err = json.Unmarshal([]byte(s1), &o1); err != nil {
		return
	}

	var o2 interface{}
	if err = json.Unmarshal([]byte(s2), &o2); err != nil {
		return
	}

	differ := gojsondiff.New()
	d, err := differ.Compare([]byte(s1), []byte(s2))
	if err != nil {
		return
	}

	config := formatter.AsciiFormatterConfig{}

	formatter := formatter.NewAsciiFormatter(o1, config)
	diff, err = formatter.Format(d)

	return cmp.Equal(o1, o2), diff, err
}

type dashboardDependencies struct {
	dashboardService influxdb.DashboardService
	userService      influxdb.UserService
	orgService       influxdb.OrganizationService
	labelService     influxdb.LabelService
	urmService       influxdb.UserResourceMappingService
}

type option func(*dashboardDependencies)

func withDashboardService(svc influxdb.DashboardService) option {
	return func(d *dashboardDependencies) {
		d.dashboardService = svc
	}
}

func withLabelService(svc influxdb.LabelService) option {
	return func(d *dashboardDependencies) {
		d.labelService = svc
	}
}
