package plan

import (
	"context"
	"net/http"
	"net/url"
	"testing"
	"time"

	genq "github.com/Khan/genqlient/graphql"
	"github.com/spf13/pflag"
	fly "github.com/superfly/fly-go"
	"github.com/superfly/fly-go/flaps"
	"github.com/superfly/flyctl/internal/flag/flagctx"
	"github.com/superfly/flyctl/internal/flapsutil"
	"github.com/superfly/flyctl/internal/flyutil"
	"github.com/superfly/flyctl/internal/mock"
	"github.com/superfly/flyctl/internal/uiex"
	"github.com/superfly/flyctl/internal/uiexutil"
	"github.com/superfly/flyctl/iostreams"
)

// mockUIEXClient implements uiexutil.Client for testing
type mockUIEXClient struct{}

func (m *mockUIEXClient) BaseURL() *url.URL {
	return nil
}

func (m *mockUIEXClient) HTTPClient() *http.Client {
	return nil
}

func (m *mockUIEXClient) ListOrganizations(ctx context.Context, admin bool) ([]uiex.Organization, error) {
	return []uiex.Organization{}, nil
}

func (m *mockUIEXClient) GetOrganization(ctx context.Context, orgSlug string) (*uiex.Organization, error) {
	return &uiex.Organization{Slug: orgSlug}, nil
}

func (m *mockUIEXClient) PromoteMachineEgressIP(ctx context.Context, appName string, egressIP string) error {
	return nil
}

// mockGenqClient implements the genq.Client interface for testing
type mockGenqClient struct{}

func (m *mockGenqClient) MakeRequest(ctx context.Context, req *genq.Request, resp *genq.Response) error {
	// Mock the GetOrganization response - just return the same slug
	// This simulates the ResolveOrganizationSlug behavior
	resp.Data = map[string]any{
		"organization": map[string]any{
			"rawSlug": "test-org", // Return a fixed value for testing
		},
	}

	return nil
}

func (m *mockUIEXClient) CreateBuild(ctx context.Context, in uiex.CreateBuildRequest) (*uiex.BuildResponse, error) {
	return &uiex.BuildResponse{}, nil
}

func (m *mockUIEXClient) FinishBuild(ctx context.Context, in uiex.FinishBuildRequest) (*uiex.BuildResponse, error) {
	return &uiex.BuildResponse{}, nil
}

func (m *mockUIEXClient) EnsureDepotBuilder(ctx context.Context, in uiex.EnsureDepotBuilderRequest) (*uiex.EnsureDepotBuilderResponse, error) {
	return &uiex.EnsureDepotBuilderResponse{}, nil
}

func (m *mockUIEXClient) CreateFlyManagedBuilder(ctx context.Context, orgSlug string, region string) (uiex.CreateFlyManagedBuilderResponse, error) {
	return uiex.CreateFlyManagedBuilderResponse{}, nil
}

func (m *mockUIEXClient) GetAllAppsCurrentReleaseTimestamps(ctx context.Context) (*map[string]time.Time, error) {
	return &map[string]time.Time{}, nil
}

func (m *mockUIEXClient) ListReleases(ctx context.Context, appName string, count int) ([]uiex.Release, error) {
	return []uiex.Release{}, nil
}

func (m *mockUIEXClient) GetCurrentRelease(ctx context.Context, appName string) (*uiex.Release, error) {
	return &uiex.Release{}, nil
}

func (m *mockUIEXClient) CreateRelease(ctx context.Context, req uiex.CreateReleaseRequest) (*uiex.Release, error) {
	return &uiex.Release{}, nil
}

func (m *mockUIEXClient) UpdateRelease(ctx context.Context, releaseID, status string, metadata any) (*uiex.Release, error) {
	return &uiex.Release{}, nil
}

func TestDefaultPostgres_ForceTypes(t *testing.T) {
	tests := []struct {
		name              string
		dbFlag            string
		mpgEnabled        bool
		mpgRegionsWithIAD bool   // whether iad region supports MPG
		expectedType      string // "managed", "unmanaged", or "default"
		expectError       bool
	}{
		{
			name:              "force managed postgres with region support",
			dbFlag:            "mpg",
			mpgEnabled:        true,
			mpgRegionsWithIAD: true,
			expectedType:      "managed",
			expectError:       false,
		},
		{
			name:              "force unmanaged postgres",
			dbFlag:            "upg",
			mpgEnabled:        true,
			mpgRegionsWithIAD: true,
			expectedType:      "unmanaged",
			expectError:       false,
		},
		{
			name:              "force legacy postgres",
			dbFlag:            "legacy",
			mpgEnabled:        true,
			mpgRegionsWithIAD: true,
			expectedType:      "unmanaged",
			expectError:       false,
		},
		{
			name:              "default non-interactive behavior with mpg enabled and region support",
			dbFlag:            "true",
			mpgEnabled:        true,
			mpgRegionsWithIAD: true,
			expectedType:      "unmanaged",
			expectError:       false,
		},
		{
			name:              "default non-interactive behavior with mpg enabled but no region support",
			dbFlag:            "true",
			mpgEnabled:        true,
			mpgRegionsWithIAD: false,
			expectedType:      "unmanaged",
			expectError:       false,
		},
		{
			name:              "default behavior with mpg disabled",
			dbFlag:            "true",
			mpgEnabled:        false,
			mpgRegionsWithIAD: false,
			expectedType:      "unmanaged",
			expectError:       false,
		},
		{
			name:              "force unmanaged overrides mpg enabled",
			dbFlag:            "upg",
			mpgEnabled:        true,
			mpgRegionsWithIAD: true,
			expectedType:      "unmanaged",
			expectError:       false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a context with iostreams
			ctx := context.Background()
			ctx = iostreams.NewContext(ctx, iostreams.System())

			// Create a test context with flags
			flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
			flagSet.String("db", tt.dbFlag, "")
			ctx = flagctx.NewContext(ctx, flagSet)

			// Set up mock flaps client for MPG region availability
			var mpgRegions []fly.Region
			if tt.mpgRegionsWithIAD {
				mpgRegions = []fly.Region{
					{Code: "iad", MPGAvailable: true},
					{Code: "lax", MPGAvailable: true},
				}
			} else {
				mpgRegions = []fly.Region{
					{Code: "lax", MPGAvailable: true},
					{Code: "fra", MPGAvailable: true},
					// iad is not in the list, so it's not available
				}
			}

			ctx = flapsutil.NewContextWithClient(ctx, &mock.FlapsClient{
				GetRegionsFunc: func(ctx context.Context) (*flaps.RegionData, error) {
					return &flaps.RegionData{Regions: mpgRegions}, nil
				},
			})

			// Set up mock API client for platform regions
			mockClient := &mock.Client{
				PlatformRegionsFunc: func(ctx context.Context) ([]fly.Region, *fly.Region, error) {
					// Return some mock regions for testing
					return []fly.Region{
						{Code: "iad", Name: "Ashburn, Virginia (US)"},
						{Code: "lax", Name: "Los Angeles, California (US)"},
						{Code: "fra", Name: "Frankfurt, Germany"},
					}, &fly.Region{Code: "iad", Name: "Ashburn, Virginia (US)"}, nil
				},
				GenqClientFunc: func() genq.Client {
					return &mockGenqClient{}
				},
			}
			ctx = flyutil.NewContextWithClient(ctx, mockClient)

			// Create a mock launch plan
			plan := &LaunchPlan{
				AppName:    "test-app",
				OrgSlug:    "test-org",
				RegionCode: "iad", // Use iad region for testing
			}

			result, err := DefaultPostgres(ctx, plan, tt.mpgEnabled)

			if tt.expectError {
				if err == nil {
					t.Errorf("expected error but got none")

					return
				}
			} else {
				if err != nil {
					t.Errorf("expected no error but got: %v", err)

					return
				}
			}

			// Check the type of postgres plan returned
			switch tt.expectedType {
			case "managed":
				if result.ManagedPostgres == nil {
					t.Errorf("expected managed postgres plan but got nil")
				}
				if result.FlyPostgres != nil {
					t.Errorf("expected no fly postgres plan but got one")
				}
			case "unmanaged":
				if result.FlyPostgres == nil {
					t.Errorf("expected fly postgres plan but got nil")
				}
				if result.ManagedPostgres != nil {
					t.Errorf("expected no managed postgres plan but got one")
				}
			}
		})
	}
}

// TestDefaultPostgres_RegionSwitching tests that when MPG region switching occurs,
// the overall LaunchPlan.RegionCode is updated, not just the postgres plan
func TestDefaultPostgres_RegionSwitching(t *testing.T) {
	t.Run("region switching updates overall app region", func(t *testing.T) {
		// Create a context with iostreams (non-interactive to avoid prompts)
		ctx := context.Background()
		ctx = iostreams.NewContext(ctx, iostreams.System())

		// Create a test context with default db flag
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.String("db", "true", "")
		ctx = flagctx.NewContext(ctx, flagSet)

		mockUIEX := &mockUIEXClient{}
		ctx = uiexutil.NewContextWithClient(ctx, mockUIEX)

		// Set up mock API client for platform regions
		mockClient := &mock.Client{
			PlatformRegionsFunc: func(ctx context.Context) ([]fly.Region, *fly.Region, error) {
				return []fly.Region{
					{Code: "iad", Name: "Ashburn, Virginia (US)"},
					{Code: "lax", Name: "Los Angeles, California (US)"},
					{Code: "fra", Name: "Frankfurt, Germany"},
				}, &fly.Region{Code: "iad", Name: "Ashburn, Virginia (US)"}, nil
			},
			GenqClientFunc: func() genq.Client {
				return &mockGenqClient{}
			},
		}
		ctx = flyutil.NewContextWithClient(ctx, mockClient)

		// Create a launch plan starting with iad region
		plan := &LaunchPlan{
			AppName:    "test-app",
			OrgSlug:    "test-org",
			RegionCode: "iad", // Start with iad
		}

		originalRegion := plan.RegionCode

		result, err := DefaultPostgres(ctx, plan, true) // mpgEnabled = true

		if err != nil {
			t.Errorf("expected no error but got: %v", err)

			return
		}

		// In non-interactive mode, it should fall back to unmanaged postgres
		// and NOT change the region (since user can't be prompted)
		if result.FlyPostgres == nil {
			t.Errorf("expected fly postgres plan but got nil")
		}
		if result.ManagedPostgres != nil {
			t.Errorf("expected no managed postgres plan but got one")
		}

		// Region should remain unchanged in non-interactive mode
		if plan.RegionCode != originalRegion {
			t.Errorf("expected region to remain %s but it changed to %s", originalRegion, plan.RegionCode)
		}
	})
}

func TestCreateFlyPostgresPlan(t *testing.T) {
	plan := &LaunchPlan{
		AppName:    "test-app",
		OrgSlug:    "test-org",
		RegionCode: "iad",
	}

	result := createFlyPostgresPlan(plan)

	if result.FlyPostgres == nil {
		t.Errorf("expected FlyPostgres plan but got nil")

		return
	}

	if result.FlyPostgres.AppName != "test-app-db" {
		t.Errorf("expected app name 'test-app-db' but got '%s'", result.FlyPostgres.AppName)
	}

	if result.FlyPostgres.VmSize != "shared-cpu-1x" {
		t.Errorf("expected vm size 'shared-cpu-1x' but got '%s'", result.FlyPostgres.VmSize)
	}

	if result.FlyPostgres.VmRam != 256 {
		t.Errorf("expected vm ram 256 but got %d", result.FlyPostgres.VmRam)
	}

	if result.FlyPostgres.DiskSizeGB != 1 {
		t.Errorf("expected disk size 1 but got %d", result.FlyPostgres.DiskSizeGB)
	}

	if result.ManagedPostgres != nil {
		t.Errorf("expected no managed postgres plan but got one")
	}
}

func TestCreateManagedPostgresPlan(t *testing.T) {
	ctx := context.Background()
	ctx = iostreams.NewContext(ctx, iostreams.System())

	plan := &LaunchPlan{
		AppName:    "test-app",
		OrgSlug:    "test-org",
		RegionCode: "iad",
	}

	result := createManagedPostgresPlan(ctx, plan, "basic")

	if result.ManagedPostgres == nil {
		t.Errorf("expected ManagedPostgres plan but got nil")

		return
	}

	if result.ManagedPostgres.DbName != "test-app-db" {
		t.Errorf("expected db name 'test-app-db' but got '%s'", result.ManagedPostgres.DbName)
	}

	if result.ManagedPostgres.Region != "iad" {
		t.Errorf("expected region 'iad' but got '%s'", result.ManagedPostgres.Region)
	}

	if result.ManagedPostgres.Plan != "basic" {
		t.Errorf("expected plan 'basic' but got '%s'", result.ManagedPostgres.Plan)
	}

	if result.ManagedPostgres.DiskSize != 10 {
		t.Errorf("expected disk size 10 but got %d", result.ManagedPostgres.DiskSize)
	}

	if result.FlyPostgres != nil {
		t.Errorf("expected no fly postgres plan but got one")
	}
}
