package handlers

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

	"github.com/go-chi/chi/v5"
	"github.com/orchard9/rdev/internal/adapter/postgres"
	"github.com/orchard9/rdev/internal/auth"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/service"
	"github.com/orchard9/rdev/internal/testutil"
)

// TestKeysHandler requires a database connection.
// Tests are skipped if the database is not available.

func setupKeysHandler(t *testing.T) (*KeysHandler, chi.Router, *auth.Service) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { testutil.CleanupTestKeys(t, db) })

	apiKeyRepo := postgres.NewAPIKeyRepository(db)
	apiKeySvc := service.NewAPIKeyService(apiKeyRepo, "test-admin-key")
	authService := auth.NewService(apiKeySvc, "test-admin-key")
	handler := NewKeysHandler(authService)

	router := chi.NewRouter()
	// For tests, we'll mount without the auth middleware
	// since we're testing the handler logic, not auth
	router.Route("/keys", func(r chi.Router) {
		r.Get("/", handler.List)
		r.Post("/", handler.Create)
		r.Get("/{id}", handler.Get)
		r.Delete("/{id}", handler.Revoke)
	})

	return handler, router, authService
}

func TestKeysHandler_List(t *testing.T) {
	_, router, authService := setupKeysHandler(t)

	// Create some test keys
	for i := 0; i < 3; i++ {
		_, err := authService.Create(context.Background(), auth.CreateKeyRequest{
			Name:      "test-handler-list-" + string(rune('a'+i)),
			Scopes:    []auth.Scope{auth.ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Failed to create test key: %v", err)
		}
	}

	req := httptest.NewRequest("GET", "/keys", nil)
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("Status = %d, want 200. Body: %s", rec.Code, rec.Body.String())
	}

	var resp map[string]any
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("Failed to decode response: %v", err)
	}

	data, ok := resp["data"].([]any)
	if !ok {
		t.Fatal("Response data is not an array")
	}

	// Should have at least 3 keys
	if len(data) < 3 {
		t.Errorf("Expected at least 3 keys, got %d", len(data))
	}
}

func TestKeysHandler_Create(t *testing.T) {
	_, router, _ := setupKeysHandler(t)

	tests := []struct {
		name       string
		body       CreateKeyRequest
		wantStatus int
		wantErr    string
	}{
		{
			name: "valid key",
			body: CreateKeyRequest{
				Name:      "test-create-key",
				Scopes:    []string{"projects:read"},
				ExpiresIn: "30d",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name: "missing name",
			body: CreateKeyRequest{
				Scopes: []string{"projects:read"},
			},
			wantStatus: http.StatusBadRequest,
			wantErr:    "name: is required",
		},
		{
			name: "missing scopes",
			body: CreateKeyRequest{
				Name: "test-no-scopes",
			},
			wantStatus: http.StatusBadRequest,
			wantErr:    "scopes: is required",
		},
		{
			name: "invalid scope",
			body: CreateKeyRequest{
				Name:   "test-invalid-scope",
				Scopes: []string{"invalid:scope"},
			},
			wantStatus: http.StatusBadRequest,
			wantErr:    "invalid scope",
		},
		{
			name: "invalid expiration",
			body: CreateKeyRequest{
				Name:      "test-invalid-exp",
				Scopes:    []string{"projects:read"},
				ExpiresIn: "invalid",
			},
			wantStatus: http.StatusBadRequest,
			wantErr:    "expiration",
		},
		{
			name: "with project restrictions",
			body: CreateKeyRequest{
				Name:       "test-with-projects",
				Scopes:     []string{"projects:read"},
				ProjectIDs: []string{"proj-a", "proj-b"},
				ExpiresIn:  "90d",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name: "never expires",
			body: CreateKeyRequest{
				Name:      "test-never-expires",
				Scopes:    []string{"admin"},
				ExpiresIn: "never",
			},
			wantStatus: http.StatusCreated,
		},
	}

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

			router.ServeHTTP(rec, req)

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

			if tt.wantErr != "" {
				if !strings.Contains(rec.Body.String(), tt.wantErr) {
					t.Errorf("Body = %q, want to contain %q", rec.Body.String(), tt.wantErr)
				}
			}

			// For successful creates, verify the response structure
			if tt.wantStatus == http.StatusCreated {
				var resp map[string]any
				json.NewDecoder(bytes.NewReader(rec.Body.Bytes())).Decode(&resp)

				data, _ := resp["data"].(map[string]any)
				if data["secret"] == nil || data["secret"] == "" {
					t.Error("Response should include secret")
				}
				if data["key"] == nil {
					t.Error("Response should include key object")
				}
			}
		})
	}
}

func TestKeysHandler_Get(t *testing.T) {
	_, router, authService := setupKeysHandler(t)

	// Create a key to get
	result, err := authService.Create(context.Background(), auth.CreateKeyRequest{
		Name:      "test-handler-get",
		Scopes:    []auth.Scope{auth.ScopeProjectsRead},
		ExpiresIn: 24 * time.Hour,
		CreatedBy: "test",
	})
	if err != nil {
		t.Fatalf("Failed to create test key: %v", err)
	}

	t.Run("existing key", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/keys/"+string(result.Key.ID), nil)
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("Status = %d, want 200. Body: %s", rec.Code, rec.Body.String())
		}

		var resp map[string]any
		json.NewDecoder(rec.Body).Decode(&resp)

		data, _ := resp["data"].(map[string]any)
		if data["name"] != "test-handler-get" {
			t.Errorf("Name = %v, want test-handler-get", data["name"])
		}
	})

	t.Run("non-existent key", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/keys/00000000-0000-0000-0000-000000000000", nil)
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusNotFound {
			t.Errorf("Status = %d, want 404", rec.Code)
		}
	})
}

func TestKeysHandler_Revoke(t *testing.T) {
	_, router, authService := setupKeysHandler(t)

	// Create a key to revoke
	result, err := authService.Create(context.Background(), auth.CreateKeyRequest{
		Name:      "test-handler-revoke",
		Scopes:    []auth.Scope{auth.ScopeProjectsRead},
		ExpiresIn: 24 * time.Hour,
		CreatedBy: "test",
	})
	if err != nil {
		t.Fatalf("Failed to create test key: %v", err)
	}

	t.Run("revoke existing key", func(t *testing.T) {
		req := httptest.NewRequest("DELETE", "/keys/"+string(result.Key.ID), nil)
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("Status = %d, want 200. Body: %s", rec.Code, rec.Body.String())
		}

		var resp map[string]any
		json.NewDecoder(rec.Body).Decode(&resp)

		data, _ := resp["data"].(map[string]any)
		if data["status"] != "revoked" {
			t.Errorf("Status = %v, want revoked", data["status"])
		}

		// Verify the key is actually revoked
		_, err := authService.Validate(context.Background(), result.Secret)
		if !errors.Is(err, auth.ErrKeyRevoked) {
			t.Errorf("Key should be revoked, got err = %v", err)
		}
	})

	t.Run("revoke non-existent key", func(t *testing.T) {
		req := httptest.NewRequest("DELETE", "/keys/00000000-0000-0000-0000-000000000000", nil)
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusNotFound {
			t.Errorf("Status = %d, want 404", rec.Code)
		}
	})
}

func TestKeysHandler_InvalidJSON(t *testing.T) {
	_, router, _ := setupKeysHandler(t)

	req := httptest.NewRequest("POST", "/keys", strings.NewReader("invalid json{"))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

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

	if !strings.Contains(rec.Body.String(), "Invalid JSON") {
		t.Errorf("Body = %q, want to contain 'Invalid JSON'", rec.Body.String())
	}
}

func TestApiKeyToResponse(t *testing.T) {
	now := time.Now()
	future := now.Add(24 * time.Hour)

	key := &auth.APIKey{
		ID:         domain.APIKeyID("test-id"),
		Name:       "test-name",
		KeyPrefix:  "rdev_sk_abc",
		Scopes:     []auth.Scope{auth.ScopeProjectsRead, auth.ScopeProjectsExecute},
		ProjectIDs: []domain.ProjectID{"proj-a"},
		CreatedAt:  now,
		ExpiresAt:  &future,
		LastUsedAt: &now,
		CreatedBy:  "test-user",
	}

	resp := apiKeyToResponse(key)

	if resp.ID != "test-id" {
		t.Errorf("ID = %q, want test-id", resp.ID)
	}
	if resp.Name != "test-name" {
		t.Errorf("Name = %q, want test-name", resp.Name)
	}
	if len(resp.Scopes) != 2 {
		t.Errorf("Scopes length = %d, want 2", len(resp.Scopes))
	}
	if len(resp.ProjectIDs) != 1 {
		t.Errorf("ProjectIDs length = %d, want 1", len(resp.ProjectIDs))
	}
	if resp.ExpiresAt == nil {
		t.Error("ExpiresAt should not be nil")
	}
	if resp.LastUsedAt == nil {
		t.Error("LastUsedAt should not be nil")
	}
	if !resp.Active {
		t.Error("Active should be true")
	}
}
