package handlers

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

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

// mockBuildAudit implements port.BuildAudit for testing.
type mockBuildAudit struct {
	entries map[string]*domain.BuildAuditEntry
	err     error
}

func newMockBuildAudit() *mockBuildAudit {
	return &mockBuildAudit{
		entries: make(map[string]*domain.BuildAuditEntry),
	}
}

func (m *mockBuildAudit) Record(_ context.Context, entry *domain.BuildAuditEntry) error {
	if m.err != nil {
		return m.err
	}
	m.entries[entry.TaskID] = entry
	return nil
}

func (m *mockBuildAudit) Update(_ context.Context, taskID string, result *domain.BuildResult) error {
	if m.err != nil {
		return m.err
	}
	entry, ok := m.entries[taskID]
	if !ok {
		return domain.ErrBuildNotFound
	}
	entry.Result = result
	if result.Success {
		entry.Status = domain.BuildStatusCompleted
	} else {
		entry.Status = domain.BuildStatusFailed
	}
	now := time.Now()
	entry.CompletedAt = &now
	return nil
}

func (m *mockBuildAudit) UpdateStatus(_ context.Context, taskID string, status domain.BuildStatus, workerID string) error {
	if m.err != nil {
		return m.err
	}
	entry, ok := m.entries[taskID]
	if !ok {
		return domain.ErrBuildNotFound
	}
	entry.Status = status
	entry.WorkerID = workerID
	return nil
}

func (m *mockBuildAudit) Get(_ context.Context, taskID string) (*domain.BuildAuditEntry, error) {
	if m.err != nil {
		return nil, m.err
	}
	entry, ok := m.entries[taskID]
	if !ok {
		return nil, domain.ErrBuildNotFound
	}
	return entry, nil
}

func (m *mockBuildAudit) List(_ context.Context, filter port.BuildAuditFilter) ([]*domain.BuildAuditEntry, error) {
	if m.err != nil {
		return nil, m.err
	}
	var result []*domain.BuildAuditEntry
	for _, entry := range m.entries {
		if filter.ProjectID != "" && entry.ProjectID != filter.ProjectID {
			continue
		}
		result = append(result, entry)
		if filter.Limit > 0 && len(result) >= filter.Limit {
			break
		}
	}
	return result, nil
}

func TestBuildsHandler_StartBuild(t *testing.T) {
	queue := newMockWorkQueue()
	audit := newMockBuildAudit()
	buildService := service.NewBuildService(queue, audit)
	handler := NewBuildsHandler(buildService)

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	tests := []struct {
		name       string
		projectID  string
		body       StartBuildRequest
		wantStatus int
	}{
		{
			name:      "valid_build",
			projectID: "my-project",
			body: StartBuildRequest{
				Prompt:      "Build a landing page with Next.js",
				Template:    "nextjs-landing",
				AutoCommit:  true,
				AutoPush:    true,
				GitCloneURL: "https://git.example.com/org/my-project.git",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name:      "missing_prompt",
			projectID: "my-project",
			body: StartBuildRequest{
				Template: "nextjs-landing",
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:      "minimal_build",
			projectID: "test-project",
			body: StartBuildRequest{
				Prompt: "Add a footer component",
			},
			wantStatus: http.StatusCreated,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body, _ := json.Marshal(tt.body)
			req := httptest.NewRequest(http.MethodPost, "/projects/"+tt.projectID+"/builds", bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			rec := httptest.NewRecorder()

			router.ServeHTTP(rec, req)

			if rec.Code != tt.wantStatus {
				t.Errorf("got status %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String())
			}

			if tt.wantStatus == http.StatusCreated {
				var resp map[string]any
				if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
					t.Fatalf("failed to unmarshal: %v", err)
				}
				data, ok := resp["data"].(map[string]any)
				if !ok {
					t.Fatalf("expected data to be map, got %T", resp["data"])
				}
				if data["task_id"] == nil || data["task_id"] == "" {
					t.Error("expected task_id in response")
				}
				if data["project_id"] != tt.projectID {
					t.Errorf("got project_id=%v, want %s", data["project_id"], tt.projectID)
				}
				if data["status"] != "pending" {
					t.Errorf("got status=%v, want pending", data["status"])
				}
			}
		})
	}
}

func TestBuildsHandler_GetBuild(t *testing.T) {
	queue := newMockWorkQueue()
	audit := newMockBuildAudit()
	buildService := service.NewBuildService(queue, audit)
	handler := NewBuildsHandler(buildService)

	// Pre-populate an audit entry
	audit.entries["task-1"] = &domain.BuildAuditEntry{
		TaskID:    "task-1",
		ProjectID: "my-project",
		WorkerID:  "worker-1",
		Spec: domain.BuildSpec{
			Prompt:   "Build landing page",
			Template: "nextjs-landing",
		},
		Status:    domain.BuildStatusCompleted,
		StartedAt: time.Now().Add(-5 * time.Minute),
		Result: &domain.BuildResult{
			Success:    true,
			CommitSHA:  "abc123",
			DurationMs: 30000,
		},
	}

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	tests := []struct {
		name       string
		taskID     string
		wantStatus int
	}{
		{
			name:       "existing_build",
			taskID:     "task-1",
			wantStatus: http.StatusOK,
		},
		{
			name:       "not_found",
			taskID:     "nonexistent",
			wantStatus: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/builds/"+tt.taskID, nil)
			rec := httptest.NewRecorder()

			router.ServeHTTP(rec, req)

			if rec.Code != tt.wantStatus {
				t.Errorf("got status %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String())
			}

			if tt.wantStatus == http.StatusOK {
				var resp map[string]any
				if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
					t.Fatalf("failed to unmarshal: %v", err)
				}
				data, ok := resp["data"].(map[string]any)
				if !ok {
					t.Fatalf("expected data to be map, got %T", resp["data"])
				}
				if data["task_id"] != "task-1" {
					t.Errorf("got task_id=%v, want task-1", data["task_id"])
				}
				if data["status"] != "completed" {
					t.Errorf("got status=%v, want completed", data["status"])
				}
			}
		})
	}
}

func TestBuildsHandler_ListBuilds(t *testing.T) {
	queue := newMockWorkQueue()
	audit := newMockBuildAudit()
	buildService := service.NewBuildService(queue, audit)
	handler := NewBuildsHandler(buildService)

	// Pre-populate audit entries
	audit.entries["task-1"] = &domain.BuildAuditEntry{
		TaskID:    "task-1",
		ProjectID: "project-a",
		Status:    domain.BuildStatusCompleted,
		Spec:      domain.BuildSpec{Prompt: "Build page"},
		StartedAt: time.Now().Add(-10 * time.Minute),
	}
	audit.entries["task-2"] = &domain.BuildAuditEntry{
		TaskID:    "task-2",
		ProjectID: "project-a",
		Status:    domain.BuildStatusRunning,
		Spec:      domain.BuildSpec{Prompt: "Add footer"},
		StartedAt: time.Now().Add(-5 * time.Minute),
	}
	audit.entries["task-3"] = &domain.BuildAuditEntry{
		TaskID:    "task-3",
		ProjectID: "project-b",
		Status:    domain.BuildStatusPending,
		Spec:      domain.BuildSpec{Prompt: "Other project"},
		StartedAt: time.Now(),
	}

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	t.Run("list_builds_for_project", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/project-a/builds", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
		}

		var resp map[string]any
		if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
			t.Fatalf("failed to unmarshal: %v", err)
		}
		data, ok := resp["data"].(map[string]any)
		if !ok {
			t.Fatalf("expected data to be map, got %T", resp["data"])
		}
		totalF, ok := data["total"].(float64)
		if !ok {
			t.Fatalf("expected total to be float64, got %T", data["total"])
		}
		if int(totalF) != 2 {
			t.Errorf("got total=%d, want 2", int(totalF))
		}
		if data["project_id"] != "project-a" {
			t.Errorf("got project_id=%v, want project-a", data["project_id"])
		}
	})

	t.Run("list_with_limit", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/project-a/builds?limit=1", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
		}

		var resp map[string]any
		if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
			t.Fatalf("failed to unmarshal: %v", err)
		}
		data, ok := resp["data"].(map[string]any)
		if !ok {
			t.Fatalf("expected data to be map, got %T", resp["data"])
		}
		totalF, ok := data["total"].(float64)
		if !ok {
			t.Fatalf("expected total to be float64, got %T", data["total"])
		}
		if int(totalF) != 1 {
			t.Errorf("got total=%d, want 1 (limited)", int(totalF))
		}
	})

	t.Run("invalid_limit", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/project-a/builds?limit=abc", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusBadRequest {
			t.Errorf("got status %d, want %d", rec.Code, http.StatusBadRequest)
		}
	})
}
