package api

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/observability/vmbackupmanager/pkg/backup"
	"github.com/tidbcloud/observability/vmbackupmanager/pkg/config"
)

// MockManager implements backup.Manager interface for testing
type MockManager struct {
	mock.Mock
}

func (m *MockManager) ListBackups() []*backup.BackupMetadata {
	args := m.Called()
	return args.Get(0).([]*backup.BackupMetadata)
}

func (m *MockManager) GetBackup(id string) (*backup.BackupMetadata, bool) {
	args := m.Called(id)
	return args.Get(0).(*backup.BackupMetadata), args.Bool(1)
}

func (m *MockManager) EnqueueBackup(backupType backup.BackupType, name string) (*backup.BackupMetadata, error) {
	args := m.Called(backupType, name)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*backup.BackupMetadata), args.Error(1)
}

func (m *MockManager) GetRunningBackupIfExists() (*backup.BackupMetadata, bool) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Bool(1)
	}
	return args.Get(0).(*backup.BackupMetadata), args.Bool(1)
}

func createTestServer() (*Server, *MockManager) {
	cfg := &config.Config{
		HttpListenAddr: ":8080",
	}

	mockManager := &MockManager{}
	server := NewServer(cfg, mockManager)

	// Set gin to test mode
	gin.SetMode(gin.TestMode)

	return server, mockManager
}

func TestNewServer(t *testing.T) {
	cfg := &config.Config{
		HttpListenAddr: ":8080",
	}
	mockManager := &MockManager{}

	server := NewServer(cfg, mockManager)

	assert.NotNil(t, server)
	assert.Equal(t, cfg, server.config)
	assert.Equal(t, mockManager, server.manager)
	assert.NotNil(t, server.engine)
	assert.NotNil(t, server.server)
	assert.Equal(t, ":8080", server.server.Addr)
}

func TestServer_ListBackups(t *testing.T) {
	server, mockManager := createTestServer()

	// Test case 1: Empty backup list
	t.Run("empty backup list", func(t *testing.T) {
		mockManager.On("ListBackups").Return([]*backup.BackupMetadata{}).Once()

		req := httptest.NewRequest("GET", "/api/v1/backups", nil)
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, float64(0), response["count"])
		assert.Empty(t, response["backups"])

		mockManager.AssertExpectations(t)
	})

	// Test case 2: Multiple backups
	t.Run("multiple backups", func(t *testing.T) {
		backups := []*backup.BackupMetadata{
			{
				Id:       "backup1",
				Type:     backup.FullBackup,
				Status:   backup.StatusCompleted,
				DestPath: "full/backup1",
			},
			{
				Id:       "backup2",
				Type:     backup.IncrementalBackup,
				Status:   backup.StatusRunning,
				DestPath: "latest",
			},
		}

		mockManager.On("ListBackups").Return(backups).Once()

		req := httptest.NewRequest("GET", "/api/v1/backups", nil)
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, float64(2), response["count"])
		assert.Len(t, response["backups"], 2)

		mockManager.AssertExpectations(t)
	})
}

func TestServer_GetBackup(t *testing.T) {
	server, mockManager := createTestServer()

	// Test case 1: Backup found
	t.Run("backup found", func(t *testing.T) {
		backup := &backup.BackupMetadata{
			Id:       "test-backup",
			Type:     backup.FullBackup,
			Status:   backup.StatusCompleted,
			DestPath: "full/test-backup",
		}

		mockManager.On("GetBackup", "test-backup").Return(backup, true).Once()

		req := httptest.NewRequest("GET", "/api/v1/backups/test-backup", nil)
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, "backup details", response["message"])
		assert.NotNil(t, response["backup"])

		backupData := response["backup"].(map[string]interface{})
		assert.Equal(t, "test-backup", backupData["id"])
		assert.Equal(t, "full", backupData["type"])

		mockManager.AssertExpectations(t)
	})

	// Test case 2: Backup not found
	t.Run("backup not found", func(t *testing.T) {
		mockManager.On("GetBackup", "nonexistent").Return((*backup.BackupMetadata)(nil), false).Once()

		req := httptest.NewRequest("GET", "/api/v1/backups/nonexistent", nil)
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusNotFound, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response["message"], "Backup with ID nonexistent not found")

		mockManager.AssertExpectations(t)
	})
}

func TestServer_CreateBackup(t *testing.T) {
	server, mockManager := createTestServer()

	// Test case 1: Successful backup creation
	t.Run("successful backup creation", func(t *testing.T) {
		createdBackup := &backup.BackupMetadata{
			Id:        "new-backup",
			Type:      backup.FullBackup,
			Status:    backup.StatusRunning,
			DestPath:  "full/new-backup",
			CreatedAt: time.Now().Format(time.RFC3339),
		}

		// No backup currently running
		mockManager.On("GetRunningBackupIfExists").Return((*backup.BackupMetadata)(nil), false).Once()
		// Successful backup creation
		mockManager.On("EnqueueBackup", backup.FullBackup, mock.AnythingOfType("string")).Return(createdBackup, nil).Once()

		req := httptest.NewRequest("POST", "/api/v1/backups", bytes.NewBuffer([]byte("{}")))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusAccepted, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, "backup created", response["message"])
		assert.NotNil(t, response["backup"])

		backupData := response["backup"].(map[string]interface{})
		assert.Equal(t, "new-backup", backupData["id"])
		assert.Equal(t, "full", backupData["type"])
		assert.Equal(t, "running", backupData["status"])

		mockManager.AssertExpectations(t)
	})

	// Test case 2: Backup already running
	t.Run("backup already running", func(t *testing.T) {
		runningBackup := &backup.BackupMetadata{
			Id:     "running-backup",
			Type:   backup.FullBackup,
			Status: backup.StatusRunning,
		}

		// Backup currently running
		mockManager.On("GetRunningBackupIfExists").Return(runningBackup, true).Once()

		req := httptest.NewRequest("POST", "/api/v1/backups", bytes.NewBuffer([]byte("{}")))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusConflict, w.Code)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response["message"], "Failed to create backup, another backup is already running: running-backup")

		mockManager.AssertExpectations(t)
	})

	// Test case 3: Enqueue backup fails
	t.Run("enqueue backup fails", func(t *testing.T) {
		// No backup currently running
		mockManager.On("GetRunningBackupIfExists").Return((*backup.BackupMetadata)(nil), false).Once()
		// Enqueue backup fails
		mockManager.On("EnqueueBackup", backup.FullBackup, mock.AnythingOfType("string")).Return((*backup.BackupMetadata)(nil), fmt.Errorf("queue is full")).Once()

		req := httptest.NewRequest("POST", "/api/v1/backups", bytes.NewBuffer([]byte("{}")))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()

		server.engine.ServeHTTP(w, req)

		assert.Equal(t, http.StatusInternalServerError, w.Code)

		var response map[string]string
		err := json.Unmarshal(w.Body.Bytes(), &response)
		assert.NoError(t, err)
		assert.Equal(t, "Failed to create backup due to an internal error.", response["message"])

		mockManager.AssertExpectations(t)
	})
}

func TestServer_HealthCheck(t *testing.T) {
	server, _ := createTestServer()

	req := httptest.NewRequest("GET", "/health", nil)
	w := httptest.NewRecorder()

	server.engine.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)

	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	require.NoError(t, err)

	assert.Equal(t, "ok", response["status"])
}

func TestServer_MetricsEndpoint(t *testing.T) {
	server, _ := createTestServer()

	req := httptest.NewRequest("GET", "/metrics", nil)
	w := httptest.NewRecorder()

	server.engine.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	// Metrics endpoint should return prometheus format
	assert.Contains(t, w.Header().Get("Content-Type"), "text/plain")
}

func TestServer_Shutdown(t *testing.T) {
	server, _ := createTestServer()

	// Create a context with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	// Start the server in a goroutine
	go func() {
		// Use a different port to avoid conflicts
		server.server.Addr = ":0" // Use any available port
		server.Start()
	}()

	// Give the server a moment to start
	time.Sleep(100 * time.Millisecond)

	// Test shutdown
	err := server.Shutdown(ctx)
	assert.NoError(t, err)
}

func TestServer_InvalidRoutes(t *testing.T) {
	server, _ := createTestServer()

	// Test invalid route
	req := httptest.NewRequest("GET", "/invalid/route", nil)
	w := httptest.NewRecorder()

	server.engine.ServeHTTP(w, req)

	assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestServer_InvalidMethods(t *testing.T) {
	server, _ := createTestServer()

	tests := []struct {
		method string
		path   string
	}{
		{"DELETE", "/api/v1/backups"},
		{"PUT", "/api/v1/backups"},
		{"PATCH", "/api/v1/backups"},
		{"DELETE", "/api/v1/backups/test"},
		{"POST", "/api/v1/backups/test"},
		{"PUT", "/api/v1/backups/test"},
	}

	for _, tt := range tests {
		t.Run(fmt.Sprintf("%s %s", tt.method, tt.path), func(t *testing.T) {
			req := httptest.NewRequest(tt.method, tt.path, nil)
			w := httptest.NewRecorder()

			server.engine.ServeHTTP(w, req)

			// Gin returns 404 when routes don't exist at all
			assert.Equal(t, http.StatusNotFound, w.Code)
		})
	}
}

func TestServer_SetupRoutes(t *testing.T) {
	server, _ := createTestServer()

	// Test that all expected routes are set up
	routes := server.engine.Routes()

	expectedRoutes := map[string]string{
		"GET /api/v1/backups":     "listBackups",
		"GET /api/v1/backups/:id": "getBackup",
		"POST /api/v1/backups":    "createBackup",
		"GET /health":             "health",
		"GET /metrics":            "metrics",
	}

	routeMap := make(map[string]bool)
	for _, route := range routes {
		key := fmt.Sprintf("%s %s", route.Method, route.Path)
		routeMap[key] = true
	}

	for expectedRoute := range expectedRoutes {
		assert.True(t, routeMap[expectedRoute], "Route %s should be registered", expectedRoute)
	}
}

func TestServer_ConcurrentRequests(t *testing.T) {
	server, mockManager := createTestServer()

	// Setup mock to handle concurrent requests
	backup := &backup.BackupMetadata{
		Id:       "test-backup",
		Type:     backup.FullBackup,
		Status:   backup.StatusCompleted,
		DestPath: "full/test-backup",
	}

	// Allow multiple calls
	mockManager.On("GetBackup", "test-backup").Return(backup, true)

	// Make concurrent requests
	const numRequests = 10
	results := make(chan int, numRequests)

	for i := 0; i < numRequests; i++ {
		go func() {
			req := httptest.NewRequest("GET", "/api/v1/backups/test-backup", nil)
			w := httptest.NewRecorder()

			server.engine.ServeHTTP(w, req)
			results <- w.Code
		}()
	}

	// Collect results
	for i := 0; i < numRequests; i++ {
		code := <-results
		assert.Equal(t, http.StatusOK, code)
	}

	// Verify all calls were made
	mockManager.AssertNumberOfCalls(t, "GetBackup", numRequests)
}
