package service

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

func TestAssignComponentPath(t *testing.T) {
	tests := []struct {
		name         string
		component    *domain.Component
		expectedPath string
	}{
		{
			name: "service gets /api/{name}",
			component: &domain.Component{
				Type: domain.ComponentTypeService,
				Name: "auth",
			},
			expectedPath: "/api/auth",
		},
		{
			name: "service with different name",
			component: &domain.Component{
				Type: domain.ComponentTypeService,
				Name: "users",
			},
			expectedPath: "/api/users",
		},
		{
			name: "app-react gets /",
			component: &domain.Component{
				Type: domain.ComponentTypeAppReact,
				Name: "web",
			},
			expectedPath: "/",
		},
		{
			name: "app-astro gets /",
			component: &domain.Component{
				Type: domain.ComponentTypeAppAstro,
				Name: "landing",
			},
			expectedPath: "/",
		},
		{
			name: "app-nextjs gets /",
			component: &domain.Component{
				Type: domain.ComponentTypeAppNextJS,
				Name: "dashboard",
			},
			expectedPath: "/",
		},
		{
			name: "worker gets empty path",
			component: &domain.Component{
				Type: domain.ComponentTypeWorker,
				Name: "processor",
			},
			expectedPath: "",
		},
		{
			name: "cli gets empty path",
			component: &domain.Component{
				Type: domain.ComponentTypeCLI,
				Name: "tool",
			},
			expectedPath: "",
		},
		{
			name: "postgres gets empty path (infrastructure)",
			component: &domain.Component{
				Type: domain.ComponentTypePostgres,
				Name: "main-db",
			},
			expectedPath: "",
		},
		{
			name: "redis gets empty path (infrastructure)",
			component: &domain.Component{
				Type: domain.ComponentTypeRedis,
				Name: "cache",
			},
			expectedPath: "",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := assignComponentPath(tc.component)
			if result != tc.expectedPath {
				t.Errorf("assignComponentPath(%s %s) = %q, want %q",
					tc.component.Type, tc.component.Name, result, tc.expectedPath)
			}
		})
	}
}

// --- Mock implementations for infrastructure component tests ---

type mockDatabaseProvisioner struct {
	createCalled       bool
	deleteCalled       bool
	getCalled          bool
	existingDB         *domain.DatabaseCredentials
	createError        error
	deleteError        error
	getError           error
	createdCredentials *domain.DatabaseCredentials
}

func (m *mockDatabaseProvisioner) CreateProjectDatabase(ctx context.Context, projectID string) (*domain.DatabaseCredentials, error) {
	m.createCalled = true
	if m.createError != nil {
		return nil, m.createError
	}
	creds := &domain.DatabaseCredentials{
		ProjectID:    projectID,
		DatabaseName: "project_" + projectID,
		Username:     "project_" + projectID,
		Password:     "testpassword",
		Host:         "localhost",
		Port:         26257,
		SSLMode:      "disable",
		URL:          "postgres://project_" + projectID + ":testpassword@localhost:26257/project_" + projectID + "?sslmode=disable",
		URLStaging:   "postgres://project_" + projectID + ":testpassword@localhost:26257/project_" + projectID + "?sslmode=disable",
		CreatedAt:    time.Now(),
	}
	m.createdCredentials = creds
	return creds, nil
}

func (m *mockDatabaseProvisioner) DeleteProjectDatabase(ctx context.Context, projectID string) error {
	m.deleteCalled = true
	return m.deleteError
}

func (m *mockDatabaseProvisioner) GetProjectDatabase(ctx context.Context, projectID string) (*domain.DatabaseCredentials, error) {
	m.getCalled = true
	if m.getError != nil {
		return nil, m.getError
	}
	return m.existingDB, nil
}

func (m *mockDatabaseProvisioner) TestConnection(ctx context.Context) error {
	return nil
}

type mockCacheProvisioner struct {
	createCalled       bool
	deleteCalled       bool
	getCalled          bool
	existingCache      *domain.CacheCredentials
	createError        error
	deleteError        error
	getError           error
	createdCredentials *domain.CacheCredentials
}

func (m *mockCacheProvisioner) CreateProjectCache(ctx context.Context, projectID string) (*domain.CacheCredentials, error) {
	m.createCalled = true
	if m.createError != nil {
		return nil, m.createError
	}
	creds := &domain.CacheCredentials{
		ProjectID:  projectID,
		URL:        "redis://proj-" + projectID + ":testpassword@localhost:6379",
		URLStaging: "redis://proj-" + projectID + ":testpassword@localhost:6379",
		Prefix:     "project:" + projectID + ":",
		Username:   "proj-" + projectID,
		Host:       "localhost",
		Port:       6379,
		CreatedAt:  time.Now(),
	}
	m.createdCredentials = creds
	return creds, nil
}

func (m *mockCacheProvisioner) DeleteProjectCache(ctx context.Context, projectID string, purgeKeys bool) error {
	m.deleteCalled = true
	return m.deleteError
}

func (m *mockCacheProvisioner) GetProjectCache(ctx context.Context, projectID string) (*domain.CacheCredentials, error) {
	m.getCalled = true
	if m.getError != nil {
		return nil, m.getError
	}
	return m.existingCache, nil
}

func (m *mockCacheProvisioner) TestConnection(ctx context.Context) error {
	return nil
}

type mockCredentialStore struct {
	stored   map[string]string
	setError error
}

func newMockCredentialStore() *mockCredentialStore {
	return &mockCredentialStore{stored: make(map[string]string)}
}

func (m *mockCredentialStore) Get(ctx context.Context, key string) (string, error) {
	return m.stored[key], nil
}

func (m *mockCredentialStore) GetRequired(ctx context.Context, key string) (string, error) {
	v, ok := m.stored[key]
	if !ok {
		return "", domain.ErrCredentialNotFound
	}
	return v, nil
}

func (m *mockCredentialStore) Set(ctx context.Context, cred domain.Credential) error {
	if m.setError != nil {
		return m.setError
	}
	m.stored[cred.Key] = cred.Value
	return nil
}

func (m *mockCredentialStore) Delete(ctx context.Context, key string) error {
	delete(m.stored, key)
	return nil
}

func (m *mockCredentialStore) List(ctx context.Context) ([]domain.Credential, error) {
	return nil, nil
}

func (m *mockCredentialStore) ListByCategory(ctx context.Context, category string) ([]domain.Credential, error) {
	return nil, nil
}

func (m *mockCredentialStore) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
	result := make(map[string]string)
	for _, k := range keys {
		if v, ok := m.stored[k]; ok {
			result[k] = v
		}
	}
	return result, nil
}

func (m *mockCredentialStore) SetMultiple(ctx context.Context, creds []domain.Credential) error {
	for _, c := range creds {
		if err := m.Set(ctx, c); err != nil {
			return err
		}
	}
	return nil
}

// --- Infrastructure component provisioning tests ---

func TestProvisionPostgres(t *testing.T) {
	tests := []struct {
		name            string
		projectID       string
		componentName   string
		dbProvisioner   *mockDatabaseProvisioner
		credStore       *mockCredentialStore
		wantErr         bool
		wantErrContains string
	}{
		{
			name:          "successful postgres provisioning",
			projectID:     "test-project",
			componentName: "main-db",
			dbProvisioner: &mockDatabaseProvisioner{},
			credStore:     newMockCredentialStore(),
			wantErr:       false,
		},
		{
			name:          "postgres already exists",
			projectID:     "test-project",
			componentName: "main-db",
			dbProvisioner: &mockDatabaseProvisioner{
				existingDB: &domain.DatabaseCredentials{ProjectID: "test-project"},
			},
			credStore:       newMockCredentialStore(),
			wantErr:         true,
			wantErrContains: "already provisioned",
		},
		{
			name:          "provisioning fails",
			projectID:     "test-project",
			componentName: "main-db",
			dbProvisioner: &mockDatabaseProvisioner{
				createError: errors.New("cockroachdb connection failed"),
			},
			credStore:       newMockCredentialStore(),
			wantErr:         true,
			wantErrContains: "failed to provision database",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			svc := &ComponentService{
				dbProvisioner:   tc.dbProvisioner,
				credentialStore: tc.credStore,
			}

			ctx := context.Background()
			component, err := svc.provisionPostgres(ctx, tc.projectID, tc.componentName)

			if tc.wantErr {
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tc.wantErrContains)
					return
				}
				if tc.wantErrContains != "" && !contains(err.Error(), tc.wantErrContains) {
					t.Errorf("error %q does not contain %q", err.Error(), tc.wantErrContains)
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if component.Type != domain.ComponentTypePostgres {
				t.Errorf("component type = %v, want %v", component.Type, domain.ComponentTypePostgres)
			}
			if component.Name != tc.componentName {
				t.Errorf("component name = %q, want %q", component.Name, tc.componentName)
			}

			// Verify credentials were stored
			if tc.credStore != nil {
				key := tc.projectID + ":DATABASE_URL"
				if _, ok := tc.credStore.stored[key]; !ok {
					t.Errorf("DATABASE_URL not stored in credential store")
				}
			}
		})
	}
}

func TestProvisionRedis(t *testing.T) {
	tests := []struct {
		name             string
		projectID        string
		componentName    string
		cacheProvisioner *mockCacheProvisioner
		credStore        *mockCredentialStore
		wantErr          bool
		wantErrContains  string
	}{
		{
			name:             "successful redis provisioning",
			projectID:        "test-project",
			componentName:    "cache",
			cacheProvisioner: &mockCacheProvisioner{},
			credStore:        newMockCredentialStore(),
			wantErr:          false,
		},
		{
			name:          "redis already exists",
			projectID:     "test-project",
			componentName: "cache",
			cacheProvisioner: &mockCacheProvisioner{
				existingCache: &domain.CacheCredentials{ProjectID: "test-project"},
			},
			credStore:       newMockCredentialStore(),
			wantErr:         true,
			wantErrContains: "already provisioned",
		},
		{
			name:          "provisioning fails",
			projectID:     "test-project",
			componentName: "cache",
			cacheProvisioner: &mockCacheProvisioner{
				createError: errors.New("redis connection failed"),
			},
			credStore:       newMockCredentialStore(),
			wantErr:         true,
			wantErrContains: "failed to provision cache",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			svc := &ComponentService{
				cacheProvisioner: tc.cacheProvisioner,
				credentialStore:  tc.credStore,
			}

			ctx := context.Background()
			component, err := svc.provisionRedis(ctx, tc.projectID, tc.componentName)

			if tc.wantErr {
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tc.wantErrContains)
					return
				}
				if tc.wantErrContains != "" && !contains(err.Error(), tc.wantErrContains) {
					t.Errorf("error %q does not contain %q", err.Error(), tc.wantErrContains)
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if component.Type != domain.ComponentTypeRedis {
				t.Errorf("component type = %v, want %v", component.Type, domain.ComponentTypeRedis)
			}
			if component.Name != tc.componentName {
				t.Errorf("component name = %q, want %q", component.Name, tc.componentName)
			}

			// Verify credentials were stored
			if tc.credStore != nil {
				key := tc.projectID + ":REDIS_URL"
				if _, ok := tc.credStore.stored[key]; !ok {
					t.Errorf("REDIS_URL not stored in credential store")
				}
			}
		})
	}
}

func TestAddInfraComponent(t *testing.T) {
	tests := []struct {
		name             string
		projectID        string
		componentType    domain.ComponentType
		componentName    string
		dbProvisioner    port.DatabaseProvisioner
		cacheProvisioner port.CacheProvisioner
		credStore        port.CredentialStore
		wantErr          bool
		wantErrContains  string
	}{
		{
			name:          "postgres component routes to provisioner",
			projectID:     "test-project",
			componentType: domain.ComponentTypePostgres,
			componentName: "main-db",
			dbProvisioner: &mockDatabaseProvisioner{},
			credStore:     newMockCredentialStore(),
			wantErr:       false,
		},
		{
			name:             "redis component routes to provisioner",
			projectID:        "test-project",
			componentType:    domain.ComponentTypeRedis,
			componentName:    "cache",
			cacheProvisioner: &mockCacheProvisioner{},
			credStore:        newMockCredentialStore(),
			wantErr:          false,
		},
		{
			name:            "postgres without provisioner fails",
			projectID:       "test-project",
			componentType:   domain.ComponentTypePostgres,
			componentName:   "main-db",
			dbProvisioner:   nil, // nil interface
			credStore:       newMockCredentialStore(),
			wantErr:         true,
			wantErrContains: "database provisioner not configured",
		},
		{
			name:             "redis without provisioner fails",
			projectID:        "test-project",
			componentType:    domain.ComponentTypeRedis,
			componentName:    "cache",
			cacheProvisioner: nil, // nil interface
			credStore:        newMockCredentialStore(),
			wantErr:          true,
			wantErrContains:  "cache provisioner not configured",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			svc := &ComponentService{
				dbProvisioner:    tc.dbProvisioner,
				cacheProvisioner: tc.cacheProvisioner,
				credentialStore:  tc.credStore,
			}

			ctx := context.Background()
			component, err := svc.addInfraComponent(ctx, tc.projectID, tc.componentType, tc.componentName)

			if tc.wantErr {
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tc.wantErrContains)
					return
				}
				if tc.wantErrContains != "" && !contains(err.Error(), tc.wantErrContains) {
					t.Errorf("error %q does not contain %q", err.Error(), tc.wantErrContains)
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if component.Type != tc.componentType {
				t.Errorf("component type = %v, want %v", component.Type, tc.componentType)
			}
		})
	}
}

func TestIsInfraComponent(t *testing.T) {
	tests := []struct {
		componentType domain.ComponentType
		want          bool
	}{
		{domain.ComponentTypePostgres, true},
		{domain.ComponentTypeRedis, true},
		{domain.ComponentTypeService, false},
		{domain.ComponentTypeWorker, false},
		{domain.ComponentTypeAppAstro, false},
		{domain.ComponentTypeAppReact, false},
		{domain.ComponentTypeCLI, false},
	}

	for _, tc := range tests {
		t.Run(string(tc.componentType), func(t *testing.T) {
			got := tc.componentType.IsInfraComponent()
			if got != tc.want {
				t.Errorf("IsInfraComponent(%s) = %v, want %v", tc.componentType, got, tc.want)
			}
		})
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
		(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}

func findSubstring(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

// Ensure mock implements interface
var _ port.DatabaseProvisioner = (*mockDatabaseProvisioner)(nil)
var _ port.CacheProvisioner = (*mockCacheProvisioner)(nil)
var _ port.CredentialStore = (*mockCredentialStore)(nil)

func TestToUpperSnake(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"auth-svc", "AUTH_SVC"},
		{"chat-svc", "CHAT_SVC"},
		{"user-service", "USER_SERVICE"},
		{"my-auth-svc-v2", "MY_AUTH_SVC_V2"},
		{"simple", "SIMPLE"},
		{"already-UPPER", "ALREADY_UPPER"},
		{"a-b-c-d", "A_B_C_D"},
		{"", ""},
	}

	for _, tc := range tests {
		t.Run(tc.input, func(t *testing.T) {
			got := toUpperSnake(tc.input)
			if got != tc.want {
				t.Errorf("toUpperSnake(%q) = %q, want %q", tc.input, got, tc.want)
			}
		})
	}
}

func TestBuildSiblingServiceURLs(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))

	tests := []struct {
		name             string
		projectID        string
		currentComponent string
		components       []domain.Component
		listErr          error
		want             map[string]string
	}{
		{
			name:             "builds URLs for sibling services",
			projectID:        "testproject",
			currentComponent: "chat-svc",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "chat-svc", Type: domain.ComponentTypeService, Port: 8002},
				{Name: "user-svc", Type: domain.ComponentTypeService, Port: 8003},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://testproject-auth-svc:8001",
				"USER_SVC_URL": "http://testproject-user-svc:8003",
			},
		},
		{
			name:             "excludes current component",
			projectID:        "myapp",
			currentComponent: "auth-svc",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
			},
			want: map[string]string{},
		},
		{
			name:             "excludes workers",
			projectID:        "myapp",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "processor", Type: domain.ComponentTypeWorker, Port: 0},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://myapp-auth-svc:8001",
			},
		},
		{
			name:             "excludes apps",
			projectID:        "myapp",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "web", Type: domain.ComponentTypeAppReact, Port: 3001},
				{Name: "landing", Type: domain.ComponentTypeAppAstro, Port: 3002},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://myapp-auth-svc:8001",
			},
		},
		{
			name:             "excludes CLI",
			projectID:        "myapp",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "tool", Type: domain.ComponentTypeCLI, Port: 0},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://myapp-auth-svc:8001",
			},
		},
		{
			name:             "excludes infrastructure components",
			projectID:        "myapp",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "main-db", Type: domain.ComponentTypePostgres, Port: 26257},
				{Name: "cache", Type: domain.ComponentTypeRedis, Port: 6379},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://myapp-auth-svc:8001",
			},
		},
		{
			name:             "excludes services with zero port",
			projectID:        "myapp",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "auth-svc", Type: domain.ComponentTypeService, Port: 8001},
				{Name: "broken-svc", Type: domain.ComponentTypeService, Port: 0},
			},
			want: map[string]string{
				"AUTH_SVC_URL": "http://myapp-auth-svc:8001",
			},
		},
		{
			name:             "handles empty component list",
			projectID:        "myapp",
			currentComponent: "api",
			components:       []domain.Component{},
			want:             map[string]string{},
		},
		{
			name:             "returns nil on list error",
			projectID:        "myapp",
			currentComponent: "api",
			listErr:          errors.New("database error"),
			want:             nil,
		},
		{
			name:             "handles hyphenated component names correctly",
			projectID:        "my-project",
			currentComponent: "api",
			components: []domain.Component{
				{Name: "user-auth-service", Type: domain.ComponentTypeService, Port: 8001},
			},
			want: map[string]string{
				"USER_AUTH_SERVICE_URL": "http://my-project-user-auth-service:8001",
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// We need to test the actual function, so we'll use a helper approach
			// by directly calling the function logic with mock data
			got := buildSiblingServiceURLsHelper(tc.components, tc.projectID, tc.currentComponent, tc.listErr, logger)

			// Compare results
			if tc.want == nil {
				if got != nil {
					t.Errorf("buildSiblingServiceURLs() = %v, want nil", got)
				}
				return
			}

			if len(got) != len(tc.want) {
				t.Errorf("buildSiblingServiceURLs() returned %d URLs, want %d", len(got), len(tc.want))
				t.Errorf("got: %v", got)
				t.Errorf("want: %v", tc.want)
				return
			}

			for key, wantURL := range tc.want {
				if gotURL, ok := got[key]; !ok {
					t.Errorf("missing key %q in result", key)
				} else if gotURL != wantURL {
					t.Errorf("URL for %q = %q, want %q", key, gotURL, wantURL)
				}
			}

			// Verify no extra keys
			for key := range got {
				if _, ok := tc.want[key]; !ok {
					t.Errorf("unexpected key %q in result", key)
				}
			}
		})
	}
}

// buildSiblingServiceURLsHelper extracts the core logic for testing without needing a full ComponentService.
// This mirrors the logic in ComponentService.buildSiblingServiceURLs.
func buildSiblingServiceURLsHelper(components []domain.Component, projectID, currentComponent string, listErr error, logger *slog.Logger) map[string]string {
	if listErr != nil {
		logger.Warn("failed to list components for sibling discovery",
			"project", projectID,
			"error", listErr,
		)
		return nil
	}

	urls := make(map[string]string)
	for _, c := range components {
		// Skip the current component, non-service types, and components without ports
		if c.Name == currentComponent || c.Type != domain.ComponentTypeService || c.Port == 0 {
			continue
		}

		// Build env var name: auth-svc -> AUTH_SVC_URL
		envKey := toUpperSnake(c.Name) + "_URL"
		// Build internal K8s service URL: http://projectid-componentname:port
		serviceName := projectID + "-" + c.Name
		urls[envKey] = "http://" + serviceName + ":" + itoa(c.Port)
	}

	return urls
}

// itoa converts int to string without importing strconv
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var digits []byte
	for n > 0 {
		digits = append([]byte{byte('0' + n%10)}, digits...)
		n /= 10
	}
	return string(digits)
}
