package handlers

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

	"github.com/go-chi/chi/v5"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// mockComponentService is a mock implementation of port.ComponentService for testing.
type mockComponentService struct {
	addComponent      func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error)
	addComponentBatch func(ctx context.Context, projectID string, reqs []port.AddComponentRequest) ([]*domain.Component, error)
	listComponents    func(ctx context.Context, projectID string) ([]domain.Component, error)
	removeComponent   func(ctx context.Context, projectID string, componentPath string) error
}

func (m *mockComponentService) AddComponent(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
	if m.addComponent != nil {
		return m.addComponent(ctx, projectID, req)
	}
	return nil, nil
}

func (m *mockComponentService) AddComponentBatch(ctx context.Context, projectID string, reqs []port.AddComponentRequest) ([]*domain.Component, error) {
	if m.addComponentBatch != nil {
		return m.addComponentBatch(ctx, projectID, reqs)
	}
	return nil, nil
}

func (m *mockComponentService) ListComponents(ctx context.Context, projectID string) ([]domain.Component, error) {
	if m.listComponents != nil {
		return m.listComponents(ctx, projectID)
	}
	return nil, nil
}

func (m *mockComponentService) RemoveComponent(ctx context.Context, projectID string, componentPath string) error {
	if m.removeComponent != nil {
		return m.removeComponent(ctx, projectID, componentPath)
	}
	return nil
}

func TestComponentsHandler_Add(t *testing.T) {
	tests := []struct {
		name           string
		projectID      string
		body           any
		setupMock      func() *mockComponentService
		expectedStatus int
		checkResponse  func(t *testing.T, body []byte)
	}{
		{
			name:      "successful add service component",
			projectID: "my-project",
			body: AddComponentRequest{
				Type: "service",
				Name: "auth-api",
			},
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
						return &domain.Component{
							Type:         domain.ComponentTypeService,
							Name:         "auth-api",
							Path:         "services/auth-api",
							Port:         8001,
							Template:     "service",
							Dependencies: []string{},
						}, nil
					},
				}
			},
			expectedStatus: http.StatusCreated,
			checkResponse: func(t *testing.T, body []byte) {
				var resp struct {
					Data ComponentResponse `json:"data"`
				}
				if err := json.Unmarshal(body, &resp); err != nil {
					t.Fatalf("failed to unmarshal response: %v", err)
				}
				if resp.Data.Type != "service" {
					t.Errorf("expected type service, got %s", resp.Data.Type)
				}
				if resp.Data.Name != "auth-api" {
					t.Errorf("expected name auth-api, got %s", resp.Data.Name)
				}
				if resp.Data.Path != "services/auth-api" {
					t.Errorf("expected path services/auth-api, got %s", resp.Data.Path)
				}
				if resp.Data.Port != 8001 {
					t.Errorf("expected port 8001, got %d", resp.Data.Port)
				}
			},
		},
		{
			name:      "missing type",
			projectID: "my-project",
			body: AddComponentRequest{
				Name: "auth-api",
			},
			setupMock:      func() *mockComponentService { return &mockComponentService{} },
			expectedStatus: http.StatusBadRequest,
		},
		{
			name:      "missing name",
			projectID: "my-project",
			body: AddComponentRequest{
				Type: "service",
			},
			setupMock:      func() *mockComponentService { return &mockComponentService{} },
			expectedStatus: http.StatusBadRequest,
		},
		{
			name:      "invalid component type",
			projectID: "my-project",
			body: AddComponentRequest{
				Type: "invalid",
				Name: "auth-api",
			},
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
						return nil, domain.ErrInvalidComponentType
					},
				}
			},
			expectedStatus: http.StatusBadRequest,
		},
		{
			name:      "duplicate component",
			projectID: "my-project",
			body: AddComponentRequest{
				Type: "service",
				Name: "auth-api",
			},
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
						return nil, domain.ErrDuplicateComponent
					},
				}
			},
			expectedStatus: http.StatusConflict,
		},
		{
			name:      "project not found",
			projectID: "nonexistent",
			body: AddComponentRequest{
				Type: "service",
				Name: "auth-api",
			},
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
						return nil, domain.ErrProjectNotFound
					},
				}
			},
			expectedStatus: http.StatusNotFound,
		},
		{
			name:           "invalid project ID",
			projectID:      "123-invalid", // starts with number
			body:           AddComponentRequest{Type: "service", Name: "auth-api"},
			setupMock:      func() *mockComponentService { return &mockComponentService{} },
			expectedStatus: http.StatusBadRequest,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mock := tc.setupMock()
			handler := NewComponentsHandler(mock)

			body, _ := json.Marshal(tc.body)
			req := httptest.NewRequest(http.MethodPost, "/projects/"+tc.projectID+"/components", bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")

			// Set up chi routing context
			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tc.projectID)
			req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

			rec := httptest.NewRecorder()
			handler.Add(rec, req)

			if rec.Code != tc.expectedStatus {
				t.Errorf("expected status %d, got %d. Body: %s", tc.expectedStatus, rec.Code, rec.Body.String())
			}

			if tc.checkResponse != nil {
				tc.checkResponse(t, rec.Body.Bytes())
			}
		})
	}
}

func TestComponentsHandler_List(t *testing.T) {
	tests := []struct {
		name           string
		projectID      string
		setupMock      func() *mockComponentService
		expectedStatus int
		checkResponse  func(t *testing.T, body []byte)
	}{
		{
			name:      "successful list",
			projectID: "my-project",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					listComponents: func(ctx context.Context, projectID string) ([]domain.Component, error) {
						return []domain.Component{
							{
								Type:         domain.ComponentTypeService,
								Name:         "auth-api",
								Path:         "services/auth-api",
								Port:         8001,
								Template:     "service",
								Dependencies: []string{},
							},
							{
								Type:         domain.ComponentTypeAppAstro,
								Name:         "landing",
								Path:         "apps/landing",
								Port:         3001,
								Template:     "app-astro",
								Dependencies: []string{},
							},
						}, nil
					},
				}
			},
			expectedStatus: http.StatusOK,
			checkResponse: func(t *testing.T, body []byte) {
				var resp struct {
					Data struct {
						Components []ComponentResponse `json:"components"`
					} `json:"data"`
				}
				if err := json.Unmarshal(body, &resp); err != nil {
					t.Fatalf("failed to unmarshal response: %v", err)
				}
				if len(resp.Data.Components) != 2 {
					t.Errorf("expected 2 components, got %d", len(resp.Data.Components))
				}
			},
		},
		{
			name:      "empty list",
			projectID: "my-project",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					listComponents: func(ctx context.Context, projectID string) ([]domain.Component, error) {
						return []domain.Component{}, nil
					},
				}
			},
			expectedStatus: http.StatusOK,
			checkResponse: func(t *testing.T, body []byte) {
				var resp struct {
					Data struct {
						Components []ComponentResponse `json:"components"`
					} `json:"data"`
				}
				if err := json.Unmarshal(body, &resp); err != nil {
					t.Fatalf("failed to unmarshal response: %v", err)
				}
				if len(resp.Data.Components) != 0 {
					t.Errorf("expected 0 components, got %d", len(resp.Data.Components))
				}
			},
		},
		{
			name:      "project not found",
			projectID: "nonexistent",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					listComponents: func(ctx context.Context, projectID string) ([]domain.Component, error) {
						return nil, domain.ErrProjectNotFound
					},
				}
			},
			expectedStatus: http.StatusNotFound,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mock := tc.setupMock()
			handler := NewComponentsHandler(mock)

			req := httptest.NewRequest(http.MethodGet, "/projects/"+tc.projectID+"/components", nil)

			// Set up chi routing context
			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tc.projectID)
			req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

			rec := httptest.NewRecorder()
			handler.List(rec, req)

			if rec.Code != tc.expectedStatus {
				t.Errorf("expected status %d, got %d. Body: %s", tc.expectedStatus, rec.Code, rec.Body.String())
			}

			if tc.checkResponse != nil {
				tc.checkResponse(t, rec.Body.Bytes())
			}
		})
	}
}

func TestComponentsHandler_Remove(t *testing.T) {
	tests := []struct {
		name           string
		projectID      string
		componentPath  string
		setupMock      func() *mockComponentService
		expectedStatus int
	}{
		{
			name:          "successful remove",
			projectID:     "my-project",
			componentPath: "services/auth-api",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					removeComponent: func(ctx context.Context, projectID string, componentPath string) error {
						return nil
					},
				}
			},
			expectedStatus: http.StatusNoContent,
		},
		{
			name:          "component not found",
			projectID:     "my-project",
			componentPath: "services/nonexistent",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					removeComponent: func(ctx context.Context, projectID string, componentPath string) error {
						return domain.ErrComponentNotFound
					},
				}
			},
			expectedStatus: http.StatusNotFound,
		},
		{
			name:          "project not found",
			projectID:     "nonexistent",
			componentPath: "services/auth-api",
			setupMock: func() *mockComponentService {
				return &mockComponentService{
					removeComponent: func(ctx context.Context, projectID string, componentPath string) error {
						return domain.ErrProjectNotFound
					},
				}
			},
			expectedStatus: http.StatusNotFound,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mock := tc.setupMock()
			handler := NewComponentsHandler(mock)

			req := httptest.NewRequest(http.MethodDelete, "/projects/"+tc.projectID+"/components/"+tc.componentPath, nil)

			// Set up chi routing context
			rctx := chi.NewRouteContext()
			rctx.URLParams.Add("id", tc.projectID)
			rctx.URLParams.Add("*", tc.componentPath)
			req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

			rec := httptest.NewRecorder()
			handler.Remove(rec, req)

			if rec.Code != tc.expectedStatus {
				t.Errorf("expected status %d, got %d. Body: %s", tc.expectedStatus, rec.Code, rec.Body.String())
			}
		})
	}
}

func TestComponentsHandler_NilService(t *testing.T) {
	handler := NewComponentsHandler(nil)

	t.Run("add with nil service", func(t *testing.T) {
		body, _ := json.Marshal(AddComponentRequest{Type: "service", Name: "auth-api"})
		req := httptest.NewRequest(http.MethodPost, "/projects/my-project/components", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")

		rctx := chi.NewRouteContext()
		rctx.URLParams.Add("id", "my-project")
		req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

		rec := httptest.NewRecorder()
		handler.Add(rec, req)

		if rec.Code != http.StatusInternalServerError {
			t.Errorf("expected status 500, got %d", rec.Code)
		}
	})

	t.Run("list with nil service", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/my-project/components", nil)

		rctx := chi.NewRouteContext()
		rctx.URLParams.Add("id", "my-project")
		req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

		rec := httptest.NewRecorder()
		handler.List(rec, req)

		if rec.Code != http.StatusInternalServerError {
			t.Errorf("expected status 500, got %d", rec.Code)
		}
	})

	t.Run("remove with nil service", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodDelete, "/projects/my-project/components/services/auth-api", nil)

		rctx := chi.NewRouteContext()
		rctx.URLParams.Add("id", "my-project")
		rctx.URLParams.Add("*", "services/auth-api")
		req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

		rec := httptest.NewRecorder()
		handler.Remove(rec, req)

		if rec.Code != http.StatusInternalServerError {
			t.Errorf("expected status 500, got %d", rec.Code)
		}
	})
}
