package jaeger

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

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
	"github.com/stretchr/testify/assert"
)

func TestJaegerClient_Services(t *testing.T) {
	tests := []struct {
		name           string
		mockResponse   string
		mockStatusCode int
		mockStatus     string
		expectedResult []string
		expectError    bool
		expectedError  error
	}{
		{
			name:           "Successful response",
			mockResponse:   `{"data": ["service1", "service2"], "total": 2, "limit": 0, "offset": 0}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{"service1", "service2"},
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Non-200 response",
			mockResponse:   "",
			mockStatusCode: http.StatusInternalServerError,
			mockStatus:     "Internal Server Error",
			expectedResult: []string{},
			expectError:    true,
			expectedError:  errors.New("Internal Server Error"),
		},
		{
			name:           "Invalid JSON response",
			mockResponse:   `{invalid json`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{},
			expectError:    true,
			expectedError:  &json.SyntaxError{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(tt.mockStatusCode)
				_, _ = w.Write([]byte(tt.mockResponse))
			}))
			defer server.Close()

			settings := backend.DataSourceInstanceSettings{
				URL: server.URL,
			}
			client, err := New(server.Client(), log.NewNullLogger(), settings)
			assert.NoError(t, err)

			services, err := client.Services()

			if tt.expectError {
				assert.Error(t, err)
				if tt.expectedError != nil {
					assert.IsType(t, tt.expectedError, err)
				}
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedResult, services)
			}
		})
	}
}

func TestJaegerClient_Operations(t *testing.T) {
	tests := []struct {
		name           string
		service        string
		mockResponse   string
		mockStatusCode int
		mockStatus     string
		expectedResult []string
		expectError    bool
		expectedError  error
	}{
		{
			name:           "Successful response",
			service:        "test-service",
			mockResponse:   `{"data": ["operation1", "operation2"], "total": 2, "limit": 0, "offset": 0}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{"operation1", "operation2"},
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Non-200 response",
			service:        "test-service",
			mockResponse:   "",
			mockStatusCode: http.StatusInternalServerError,
			mockStatus:     "Internal Server Error",
			expectedResult: []string{},
			expectError:    true,
			expectedError:  errors.New("Internal Server Error"),
		},
		{
			name:           "Invalid JSON response",
			service:        "test-service",
			mockResponse:   `{invalid json`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{},
			expectError:    true,
			expectedError:  &json.SyntaxError{},
		},
		{
			name:           "Service with special characters",
			service:        "test/service:1",
			mockResponse:   `{"data": ["operation1"], "total": 1, "limit": 0, "offset": 0}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{"operation1"},
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Empty service",
			service:        "",
			mockResponse:   `{"data": [], "total": 0, "limit": 0, "offset": 0}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedResult: []string{},
			expectError:    false,
			expectedError:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(tt.mockStatusCode)
				_, _ = w.Write([]byte(tt.mockResponse))
			}))
			defer server.Close()

			settings := backend.DataSourceInstanceSettings{
				URL: server.URL,
			}
			client, err := New(server.Client(), log.NewNullLogger(), settings)
			assert.NoError(t, err)

			operations, err := client.Operations(tt.service)

			if tt.expectError {
				assert.Error(t, err)
				if tt.expectedError != nil {
					assert.IsType(t, tt.expectedError, err)
				}
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedResult, operations)
			}
		})
	}
}

func TestJaegerClient_Search(t *testing.T) {
	tests := []struct {
		name           string
		query          *JaegerQuery
		start          int64
		end            int64
		mockResponse   string
		mockStatusCode int
		expectedURL    string
		expectError    bool
		expectedError  error
	}{
		{
			name: "Preserves base path in Jaeger URL",
			query: &JaegerQuery{
				Service: "test-service",
			},
			start:          1735689600000000,
			end:            1738368000000000,
			mockResponse:   `{"data":[{"traceID":"test-trace-id"}]}`,
			mockStatusCode: http.StatusOK,
			expectedURL:    "/abc/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name: "Successful search with all parameters",
			query: &JaegerQuery{
				Service:     "test-service",
				Operation:   "test-operation",
				Tags:        "error=true",
				MinDuration: "1s",
				MaxDuration: "5s",
				Limit:       10,
			},
			start:          1735689600000000,
			end:            1738368000000000,
			mockResponse:   `{"data":[{"traceID":"test-trace-id"}]}`,
			mockStatusCode: http.StatusOK,
			expectedURL:    "/api/traces?end=1738368000000000&limit=10&maxDuration=5s&minDuration=1s&operation=test-operation&service=test-service&start=1735689600000000&tags=%7B%22error%22%3A%22true%22%7D",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name: "Successful search with minimal parameters",
			query: &JaegerQuery{
				Service: "test-service",
			},
			start:          1735689600000000,
			end:            1738368000000000,
			mockResponse:   `{"data":[{"traceID":"test-trace-id"}]}`,
			mockStatusCode: http.StatusOK,
			expectedURL:    "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name: "Server error",
			query: &JaegerQuery{
				Service: "test-service",
			},
			start:          1735689600000000,
			end:            1738368000000000,
			mockResponse:   "",
			mockStatusCode: http.StatusInternalServerError,
			expectedURL:    "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
			expectError:    true,
			expectedError:  backend.DownstreamError(fmt.Errorf("request failed: %s", http.StatusText(http.StatusInternalServerError))),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var actualURL string
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				actualURL = r.URL.String()
				w.WriteHeader(tt.mockStatusCode)
				_, _ = w.Write([]byte(tt.mockResponse))
			}))
			defer server.Close()

			settings := backend.DataSourceInstanceSettings{
				URL: server.URL,
			}

			if tt.name == "Preserves base path in Jaeger URL" {
				settings.URL = server.URL + "/abc"
			}

			client, err := New(server.Client(), log.NewNullLogger(), settings)
			assert.NoError(t, err)
			traces, err := client.Search(tt.query, tt.start, tt.end)

			if tt.expectError {
				assert.Error(t, err)
				if tt.expectedError != nil {
					assert.IsType(t, tt.expectedError, err)
				}
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, traces)
				assert.Equal(t, tt.expectedURL, actualURL)
			}
		})
	}
}

func TestJaegerClient_Trace(t *testing.T) {
	tests := []struct {
		name           string
		traceId        string
		jsonData       string
		start          int64
		end            int64
		mockResponse   string
		mockStatusCode int
		mockStatus     string
		expectedURL    string
		expectError    bool
		expectedError  error
	}{
		{
			name:           "Successful response with time params enabled",
			traceId:        "abc123",
			jsonData:       `{"traceIdTimeParams": {"enabled": true}}`,
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[{"traceID":"abc123"}]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/traces/abc123?end=2000&start=1000",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Successful response with time params disabled",
			traceId:        "abc123",
			jsonData:       `{"traceIdTimeParams": {"enabled": false}}`,
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[{"traceID":"abc123"}]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/traces/abc123",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Non-200 response",
			traceId:        "abc123",
			jsonData:       `{"traceIdTimeParams": {"enabled": true}}`,
			start:          1000,
			end:            2000,
			mockResponse:   "",
			mockStatusCode: http.StatusInternalServerError,
			mockStatus:     "Internal Server Error",
			expectedURL:    "/api/traces/abc123?end=2000&start=1000",
			expectError:    true,
			expectedError:  backend.PluginError(errors.New("Internal Server Error")),
		},
		{
			name:           "Invalid JSON response",
			traceId:        "abc123",
			jsonData:       `{"traceIdTimeParams": {"enabled": true}}`,
			start:          1000,
			end:            2000,
			mockResponse:   `{invalid json`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/traces/abc123?end=2000&start=1000",
			expectError:    true,
			expectedError:  &json.SyntaxError{},
		},
		{
			name:           "Empty trace ID",
			traceId:        "",
			jsonData:       `{"traceIdTimeParams": {"enabled": true}}`,
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "",
			expectError:    true,
			expectedError:  backend.DownstreamError(errors.New("traceID is empty")),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var actualURL string
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				actualURL = r.URL.String()
				w.WriteHeader(tt.mockStatusCode)
				_, _ = w.Write([]byte(tt.mockResponse))
			}))
			defer server.Close()

			settings := backend.DataSourceInstanceSettings{
				URL:      server.URL,
				JSONData: []byte(tt.jsonData),
			}
			client, err := New(server.Client(), log.NewNullLogger(), settings)
			assert.NoError(t, err)

			trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end, "A")

			if tt.expectError {
				assert.Error(t, err)
				if tt.expectedError != nil {
					assert.IsType(t, tt.expectedError, err)
				}
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, trace)
			}
			assert.Equal(t, tt.expectedURL, actualURL)
		})
	}
}

func TestJaegerClient_Dependencies(t *testing.T) {
	tests := []struct {
		name           string
		start          int64
		end            int64
		mockResponse   string
		mockStatusCode int
		mockStatus     string
		expectedURL    string
		expectError    bool
		expectedError  error
	}{
		{
			name:           "Successful response with time range",
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/dependencies?endTs=2000&lookback=1000",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Successful response without time range",
			start:          0,
			end:            0,
			mockResponse:   `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/dependencies",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Non-200 response",
			start:          1000,
			end:            2000,
			mockResponse:   "",
			mockStatusCode: http.StatusInternalServerError,
			mockStatus:     "Internal Server Error",
			expectedURL:    "/api/dependencies?endTs=2000&lookback=1000",
			expectError:    true,
			expectedError:  backend.PluginError(errors.New("Internal Server Error")),
		},
		{
			name:           "Invalid JSON response",
			start:          1000,
			end:            2000,
			mockResponse:   `{invalid json`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/dependencies?endTs=2000&lookback=1000",
			expectError:    true,
			expectedError:  &json.SyntaxError{},
		},
		{
			name:           "Empty dependencies response and no errors",
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/dependencies?endTs=2000&lookback=1000",
			expectError:    false,
			expectedError:  nil,
		},
		{
			name:           "Response with errors",
			start:          1000,
			end:            2000,
			mockResponse:   `{"data":[],"errors":[{"code":500,"msg":"Internal error"}]}`,
			mockStatusCode: http.StatusOK,
			mockStatus:     "OK",
			expectedURL:    "/api/dependencies?endTs=2000&lookback=1000",
			expectError:    false,
			expectedError:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var actualURL string
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				actualURL = r.URL.String()
				w.WriteHeader(tt.mockStatusCode)
				_, _ = w.Write([]byte(tt.mockResponse))
			}))
			defer server.Close()

			settings := backend.DataSourceInstanceSettings{
				URL: server.URL,
			}
			client, err := New(server.Client(), log.NewNullLogger(), settings)
			assert.NoError(t, err)

			dependencies, err := client.Dependencies(context.Background(), tt.start, tt.end)

			if tt.expectError {
				assert.Error(t, err)
				if tt.expectedError != nil {
					assert.IsType(t, tt.expectedError, err)
				}
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, dependencies)
			}
			assert.Equal(t, tt.expectedURL, actualURL)
		})
	}
}
