package api

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/api/routing"
	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
	"github.com/grafana/grafana/pkg/services/dashboardimport"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/quota"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/web/webtest"
)

func TestImportDashboardAPI(t *testing.T) {
	t.Run("Quota not reached, schema loader service disabled", func(t *testing.T) {
		importDashboardServiceCalled := false
		service := &serviceMock{
			importDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
				importDashboardServiceCalled = true
				return nil, nil
			},
		}

		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		t.Run("Not signed in should return 404", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
		})

		t.Run("Signed in, empty plugin id and dashboard model empty should return error", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{
				PluginId:  "",
				Dashboard: nil,
			}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
			webtest.RequestWithSignedInUser(req, &user.SignedInUser{
				UserID: 1,
			})
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
		})

		t.Run("Signed in, dashboard model set should call import dashboard service", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{
				Dashboard: simplejson.New(),
			}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
			webtest.RequestWithSignedInUser(req, &user.SignedInUser{
				UserID: 1,
			})
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusOK, resp.StatusCode)
			require.True(t, importDashboardServiceCalled)
		})

		t.Run("Signed in, dashboard model set, trimdefaults enabled should not call schema loader service", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{
				Dashboard: simplejson.New(),
			}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
			webtest.RequestWithSignedInUser(req, &user.SignedInUser{
				UserID: 1,
			})
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusOK, resp.StatusCode)
			require.True(t, importDashboardServiceCalled)
		})
	})

	t.Run("Quota not reached, schema loader service enabled", func(t *testing.T) {
		importDashboardServiceCalled := false
		service := &serviceMock{
			importDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
				importDashboardServiceCalled = true
				return nil, nil
			},
		}

		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		t.Run("Signed in, dashboard model set, trimdefaults enabled should call schema loader service", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{
				Dashboard: simplejson.New(),
			}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
			webtest.RequestWithSignedInUser(req, &user.SignedInUser{
				UserID: 1,
				Permissions: map[int64]map[string][]string{
					1: {dashboards.ActionDashboardsCreate: {}},
				},
			})
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusOK, resp.StatusCode)
			require.True(t, importDashboardServiceCalled)
		})
	})

	t.Run("Quota reached", func(t *testing.T) {
		service := &serviceMock{}
		importDashboardAPI := New(service, quotaServiceFunc(quotaReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		t.Run("Signed in, dashboard model set, should return 403 forbidden/quota reached", func(t *testing.T) {
			cmd := &dashboardimport.ImportDashboardRequest{
				Dashboard: simplejson.New(),
			}
			jsonBytes, err := json.Marshal(cmd)
			require.NoError(t, err)
			req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
			webtest.RequestWithSignedInUser(req, &user.SignedInUser{
				UserID: 1,
			})
			resp, err := s.SendJSON(req)
			require.NoError(t, err)
			require.NoError(t, resp.Body.Close())
			require.Equal(t, http.StatusForbidden, resp.StatusCode)
		})
	})
}

func TestInterpolateDashboardFeatureFlag(t *testing.T) {
	t.Run("Feature flag disabled - interpolate endpoint should return 404", func(t *testing.T) {
		service := &serviceMock{}
		// Create features with dashboardLibrary disabled
		features := featuremgmt.WithFeatures()
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			Dashboard: simplejson.New(),
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusNotFound, resp.StatusCode)
	})

	t.Run("dashboardLibrary feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
		interpolateDashboardServiceCalled := false
		service := &serviceMock{
			interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
				interpolateDashboardServiceCalled = true
				return simplejson.New(), nil
			},
		}
		// Create features with dashboardLibrary enabled
		features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			PluginId: "test-plugin",
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.True(t, interpolateDashboardServiceCalled)
	})

	t.Run("suggestedDashboards feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
		interpolateDashboardServiceCalled := false
		service := &serviceMock{
			interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
				interpolateDashboardServiceCalled = true
				return simplejson.New(), nil
			},
		}
		// Create features with suggestedDashboards enabled
		features := featuremgmt.WithFeatures(featuremgmt.FlagSuggestedDashboards)
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			PluginId: "test-plugin",
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.True(t, interpolateDashboardServiceCalled)
	})
}

func TestInterpolateDashboardAPI(t *testing.T) {
	features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)

	t.Run("Backward compatibility - plugin-based flow still works", func(t *testing.T) {
		var capturedReq *dashboardimport.ImportDashboardRequest
		interpolateDashboardServiceCalled := false
		service := &serviceMock{
			interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
				interpolateDashboardServiceCalled = true
				capturedReq = req
				result := simplejson.New()
				result.Set("title", "Test Dashboard")
				return result, nil
			},
		}
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			PluginId: "test-plugin",
			Path:     "dashboards/test.json",
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.True(t, interpolateDashboardServiceCalled)
		require.NotNil(t, capturedReq)
		require.Equal(t, "test-plugin", capturedReq.PluginId)
		require.Equal(t, "dashboards/test.json", capturedReq.Path)
	})

	t.Run("New flow with dashboard JSON - should call service with dashboard", func(t *testing.T) {
		var capturedReq *dashboardimport.ImportDashboardRequest
		interpolateDashboardServiceCalled := false
		service := &serviceMock{
			interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
				interpolateDashboardServiceCalled = true
				capturedReq = req
				result := simplejson.New()
				result.Set("title", "Community Dashboard")
				result.Set("panels", []interface{}{})
				return result, nil
			},
		}
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		// Create a test dashboard with datasource that needs interpolation
		testDashboard := simplejson.New()
		testDashboard.Set("title", "Test Community Dashboard")
		testDashboard.Set("panels", []interface{}{
			map[string]interface{}{
				"datasource": "${DS_PROMETHEUS}",
			},
		})

		cmd := &dashboardimport.ImportDashboardRequest{
			Dashboard: testDashboard,
			Inputs: []dashboardimport.ImportDashboardInput{
				{Name: "DS_PROMETHEUS", Type: "datasource", PluginId: "prometheus", Value: "my-prometheus"},
			},
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.True(t, interpolateDashboardServiceCalled)
		require.NotNil(t, capturedReq)
		require.NotNil(t, capturedReq.Dashboard)
		require.Len(t, capturedReq.Inputs, 1)
		require.Equal(t, "DS_PROMETHEUS", capturedReq.Inputs[0].Name)
	})

	t.Run("Validation - both pluginId and dashboard missing should return error", func(t *testing.T) {
		service := &serviceMock{}
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			PluginId:  "",
			Dashboard: nil,
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
	})

	t.Run("Response should not include internal fields", func(t *testing.T) {
		service := &serviceMock{
			interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
				result := simplejson.New()
				result.Set("title", "Test Dashboard")
				result.Set("__elements", map[string]interface{}{"test": "value"})
				result.Set("__inputs", []interface{}{})
				result.Set("__requires", []interface{}{})
				result.Set("panels", []interface{}{})
				return result, nil
			},
		}
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			PluginId: "test-plugin",
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		webtest.RequestWithSignedInUser(req, &user.SignedInUser{
			UserID: 1,
			Permissions: map[int64]map[string][]string{
				1: {dashboards.ActionDashboardsCreate: {}},
			},
		})
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		defer func() { require.NoError(t, resp.Body.Close()) }()
		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Parse response body and verify internal fields are removed
		var responseBody map[string]interface{}
		err = json.NewDecoder(resp.Body).Decode(&responseBody)
		require.NoError(t, err)
		require.Equal(t, "Test Dashboard", responseBody["title"])
		require.NotContains(t, responseBody, "__elements")
		require.NotContains(t, responseBody, "__inputs")
		require.NotContains(t, responseBody, "__requires")
	})

	t.Run("Not signed in should return 401", func(t *testing.T) {
		service := &serviceMock{}
		importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)

		routeRegister := routing.NewRouteRegister()
		importDashboardAPI.RegisterAPIEndpoints(routeRegister)
		s := webtest.NewServer(t, routeRegister)

		cmd := &dashboardimport.ImportDashboardRequest{
			Dashboard: simplejson.New(),
		}
		jsonBytes, err := json.Marshal(cmd)
		require.NoError(t, err)
		req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
		resp, err := s.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})
}

type serviceMock struct {
	importDashboardFunc      func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error)
	interpolateDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error)
}

func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
	if s.importDashboardFunc != nil {
		return s.importDashboardFunc(ctx, req)
	}

	return nil, nil
}

func (s *serviceMock) InterpolateDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
	if s.interpolateDashboardFunc != nil {
		return s.interpolateDashboardFunc(ctx, req)
	}

	return simplejson.New(), nil
}

func quotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) {
	return true, nil
}

func quotaNotReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) {
	return false, nil
}
