package org

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/maas/availability"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
)

type hostedModelOrganizationProvider struct {
	routeOrganizationProvider

	settingsUserID         string
	settingsOrganizationID string
	settingsParams         organization.UpdateOrganizationHostedModelSettingsParams

	authorizationUserID         string
	authorizationOrganizationID string
	authorizationWorkspaceID    string
	authorizationParams         organization.UpsertOrganizationWorkspaceHostedModelAuthorizationParams

	previewUserID         string
	previewOrganizationID string
	previewWorkspaceID    string
	previewParams         organization.UpsertOrganizationWorkspaceHostedModelAuthorizationParams
}

func (provider *hostedModelOrganizationProvider) GetOrganizationHostedModelSettingsForUser(_ context.Context, userID string, organizationID string) (organization.OrganizationHostedModelSettingsView, error) {
	provider.settingsUserID = userID
	provider.settingsOrganizationID = organizationID
	return organization.OrganizationHostedModelSettingsView{
		OrganizationID:             organizationID,
		PickerVisibilityPolicy:     "show_disabled_when_unavailable",
		AutoAuthorizationEnabled:   true,
		AutoAuthorizationMode:      "all_active_hosted_inventory",
		AutoAuthorizationModelRefs: []string{},
		UpdatedAt:                  time.Unix(100, 0).UTC(),
	}, nil
}

func (provider *hostedModelOrganizationProvider) PatchOrganizationHostedModelSettingsForUser(_ context.Context, userID string, organizationID string, params organization.UpdateOrganizationHostedModelSettingsParams) (organization.OrganizationHostedModelSettingsView, error) {
	provider.settingsUserID = userID
	provider.settingsOrganizationID = organizationID
	provider.settingsParams = params
	return organization.OrganizationHostedModelSettingsView{
		OrganizationID:             organizationID,
		PickerVisibilityPolicy:     derefString(params.PickerVisibilityPolicy),
		AutoAuthorizationEnabled:   derefBool(params.AutoAuthorizationEnabled),
		AutoAuthorizationMode:      derefString(params.AutoAuthorizationMode),
		AutoAuthorizationModelRefs: derefStringSlice(params.AutoAuthorizationModelRefs),
		UpdatedAt:                  time.Unix(100, 0).UTC(),
	}, nil
}

func (provider *hostedModelOrganizationProvider) UpsertOrganizationWorkspaceHostedModelAuthorizationForUser(_ context.Context, userID string, organizationID string, workspaceID string, params organization.UpsertOrganizationWorkspaceHostedModelAuthorizationParams) (organization.OrganizationWorkspaceHostedModelAuthorizationView, error) {
	provider.authorizationUserID = userID
	provider.authorizationOrganizationID = organizationID
	provider.authorizationWorkspaceID = workspaceID
	provider.authorizationParams = params
	return organization.OrganizationWorkspaceHostedModelAuthorizationView{
		OrganizationID:           organizationID,
		WorkspaceID:              workspaceID,
		AuthorizationMode:        params.AuthorizationMode,
		SelectedModelRefs:        params.SelectedModelRefs,
		Status:                   "active",
		Configured:               true,
		AffectedHostedAgentCount: 2,
		CreatedAt:                time.Unix(100, 0).UTC(),
		UpdatedAt:                time.Unix(101, 0).UTC(),
	}, nil
}

func (provider *hostedModelOrganizationProvider) PreviewOrganizationWorkspaceHostedModelAuthorizationForUser(_ context.Context, userID string, organizationID string, workspaceID string, params organization.UpsertOrganizationWorkspaceHostedModelAuthorizationParams) (organization.OrganizationWorkspaceHostedModelAuthorizationView, error) {
	provider.previewUserID = userID
	provider.previewOrganizationID = organizationID
	provider.previewWorkspaceID = workspaceID
	provider.previewParams = params
	return organization.OrganizationWorkspaceHostedModelAuthorizationView{
		OrganizationID:           organizationID,
		WorkspaceID:              workspaceID,
		AuthorizationMode:        params.AuthorizationMode,
		SelectedModelRefs:        params.SelectedModelRefs,
		Status:                   "active",
		Configured:               true,
		AffectedHostedAgentCount: 3,
		CreatedAt:                time.Unix(100, 0).UTC(),
		UpdatedAt:                time.Unix(101, 0).UTC(),
	}, nil
}

type hostedModelAvailabilityClient struct {
	organizationID string
	availability   availability.Availability
	err            error
}

func (client *hostedModelAvailabilityClient) LoadHostedAvailability(_ context.Context, organizationID string) (availability.Availability, error) {
	client.organizationID = organizationID
	if client.err != nil {
		return availability.Availability{}, client.err
	}
	return client.availability, nil
}

func int64Ptr(value int64) *int64 {
	return &value
}

func TestHostedModelHandlerModelsListReturnsOrgInventory(t *testing.T) {
	t.Parallel()

	provider := &hostedModelOrganizationProvider{}
	hosted := &hostedModelAvailabilityClient{
		availability: hostedModelAvailability(),
	}
	handler := &HostedModelHandler{organizationHandlerBase: organizationHandlerBase{
		organizations: provider,
		hostedModels:  hosted,
	}}

	response, err := handler.ModelsList(hostedModelAuthenticatedRequest(t, "/api/v1/orgs/org_1/hosted-models", "org_1"))
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)

	body := response.Body.(dto.OrganizationHostedModelsResponse)
	require.Equal(t, orgHostedModelsObject, body.Object)
	require.Equal(t, "org_1", body.OrganizationID)
	require.Equal(t, orgHostedAvailabilityStatusAvailable, body.HostedAvailability.Status)
	require.Equal(t, availability.CreditAtomsPerCredit, body.HostedAvailability.CreditAtomsPerCredit)
	require.Equal(t, "eligible_entitled", body.EntitlementStatus)
	require.Equal(t, "positive_balance", body.BalanceStatus)
	require.Equal(t, int64(7500), body.AvailableCreditAtoms)
	require.Equal(t, "credit_atom", body.CreditUnit)
	require.Equal(t, "usr_1", provider.settingsUserID)
	require.Equal(t, "org_1", provider.settingsOrganizationID)
	require.Equal(t, "org_1", hosted.organizationID)
	require.Len(t, body.Models, 1)
	require.Equal(t, "hm_deepseek_v4_pro", body.Models[0].HostedModelID)
	require.Equal(t, "agentteam/deepseek-v4-pro", body.Models[0].ModelReference)
	require.True(t, body.Models[0].Enabled)
	require.Equal(t, "default_open", body.Models[0].AccessSource)
	require.Equal(t, []string{"messages", "responses"}, body.Models[0].SupportedFamilies)
	require.Equal(t, []string{"responses"}, body.Models[0].ExecutableWireAPIFamilies)
	require.Equal(t, []string{"tool_use"}, body.Models[0].MessagesFeatures)
	require.Equal(t, []string{"json_object"}, body.Models[0].ChatCompletionsFeatures)
	require.Equal(t, []string{"text_format"}, body.Models[0].ResponsesFeatures)
	require.NotNil(t, body.Models[0].InputCreditAtomsPerMillionTokens)
	require.NotNil(t, body.Models[0].CacheReadInputCreditAtomsPerMillionTokens)
	require.NotNil(t, body.Models[0].OutputCreditAtomsPerMillionTokens)
	require.Equal(t, int64(100), *body.Models[0].InputCreditAtomsPerMillionTokens)
	require.Equal(t, int64(40), *body.Models[0].CacheReadInputCreditAtomsPerMillionTokens)
	require.Equal(t, int64(200), *body.Models[0].OutputCreditAtomsPerMillionTokens)
	require.Equal(t, "hosted_token_context_threshold_v1", body.Models[0].PricingFormulaVersion)
	require.Equal(t, []string{"input_tokens", "cache_read_input_tokens", "output_tokens"}, body.Models[0].BillableDimensions)
	require.Len(t, body.Models[0].ContextTiers, 1)
	require.NotNil(t, body.Models[0].ContextTiers[0].MaxContextTokens)
	require.Equal(t, int64(4096), *body.Models[0].ContextTiers[0].MaxContextTokens)
}

func TestHostedModelHandlerModelsListReturnsUnavailableWhenMaaSFails(t *testing.T) {
	t.Parallel()

	provider := &hostedModelOrganizationProvider{}
	hosted := &hostedModelAvailabilityClient{err: errors.New("agentteam-maas unavailable")}
	handler := &HostedModelHandler{organizationHandlerBase: organizationHandlerBase{
		organizations: provider,
		hostedModels:  hosted,
	}}

	response, err := handler.ModelsList(hostedModelAuthenticatedRequest(t, "/api/v1/orgs/org_1/hosted-models", "org_1"))
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)

	body := response.Body.(dto.OrganizationHostedModelsResponse)
	require.Equal(t, orgHostedAvailabilityStatusUnavailable, body.HostedAvailability.Status)
	require.Equal(t, []string{orgHostedReasonTemporarilyUnavailable}, body.HostedAvailability.Reasons)
	require.Empty(t, body.Models)
	require.Equal(t, "org_1", hosted.organizationID)
}

func TestHostedModelHandlerSettingsPatchBindsDTO(t *testing.T) {
	t.Parallel()

	provider := &hostedModelOrganizationProvider{}
	handler := &HostedModelHandler{organizationHandlerBase: organizationHandlerBase{organizations: provider}}
	router := gin.New()
	router.PATCH("/api/v1/orgs/:orgId/hosted-model-settings", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleOrgAuthJSON[dto.OrganizationHostedModelSettingsPatchRequest](handler.SettingsPatch))

	recorder := httptest.NewRecorder()
	body := `{"pickerVisibilityPolicy":"hide_disabled_when_unavailable","autoAuthorizationEnabled":true,"autoAuthorizationMode":"selected_hosted_models","autoAuthorizationModelRefs":["agentteam/deepseek-v4-pro","agentteam/deepseek-v4-flash"]}`
	request := httptest.NewRequest(http.MethodPatch, "/api/v1/orgs/org_1/hosted-model-settings", bytes.NewBufferString(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "usr_1", provider.settingsUserID)
	require.Equal(t, "org_1", provider.settingsOrganizationID)
	require.NotNil(t, provider.settingsParams.PickerVisibilityPolicy)
	require.Equal(t, "hide_disabled_when_unavailable", *provider.settingsParams.PickerVisibilityPolicy)
	require.NotNil(t, provider.settingsParams.AutoAuthorizationEnabled)
	require.True(t, *provider.settingsParams.AutoAuthorizationEnabled)
	require.NotNil(t, provider.settingsParams.AutoAuthorizationMode)
	require.Equal(t, "selected_hosted_models", *provider.settingsParams.AutoAuthorizationMode)
	require.NotNil(t, provider.settingsParams.AutoAuthorizationModelRefs)
	require.Equal(t, []string{"agentteam/deepseek-v4-pro", "agentteam/deepseek-v4-flash"}, *provider.settingsParams.AutoAuthorizationModelRefs)
}

func TestHostedModelHandlerWorkspaceAuthorizationPreviewBindsDTO(t *testing.T) {
	t.Parallel()

	provider := &hostedModelOrganizationProvider{}
	handler := &HostedModelHandler{organizationHandlerBase: organizationHandlerBase{organizations: provider}}
	router := gin.New()
	router.POST("/api/v1/orgs/:orgId/workspaces/:workspaceId/hosted-model-authorization/preview", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleOrgAuthJSON[dto.OrganizationWorkspaceHostedModelAuthorizationPutRequest](handler.WorkspaceAuthorizationPreview))

	recorder := httptest.NewRecorder()
	body := `{"authorizationMode":"selected_hosted_models","selectedModelRefs":["agentteam/deepseek-v4-pro"]}`
	request := httptest.NewRequest(http.MethodPost, "/api/v1/orgs/org_1/workspaces/ws_1/hosted-model-authorization/preview", bytes.NewBufferString(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "usr_1", provider.previewUserID)
	require.Equal(t, "org_1", provider.previewOrganizationID)
	require.Equal(t, "ws_1", provider.previewWorkspaceID)
	require.Equal(t, "selected_hosted_models", provider.previewParams.AuthorizationMode)
	require.Equal(t, []string{"agentteam/deepseek-v4-pro"}, provider.previewParams.SelectedModelRefs)
	require.Contains(t, recorder.Body.String(), `"affectedHostedAgentCount":3`)
}

func TestHostedModelHandlerWorkspaceAuthorizationPutBindsDTO(t *testing.T) {
	t.Parallel()

	provider := &hostedModelOrganizationProvider{}
	handler := &HostedModelHandler{organizationHandlerBase: organizationHandlerBase{organizations: provider}}
	router := gin.New()
	router.PUT("/api/v1/orgs/:orgId/workspaces/:workspaceId/hosted-model-authorization", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleOrgAuthJSON[dto.OrganizationWorkspaceHostedModelAuthorizationPutRequest](handler.WorkspaceAuthorizationPut))

	recorder := httptest.NewRecorder()
	body := `{"authorizationMode":"selected_hosted_models","selectedModelRefs":["agentteam/deepseek-v4-pro"]}`
	request := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/org_1/workspaces/ws_1/hosted-model-authorization", bytes.NewBufferString(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "usr_1", provider.authorizationUserID)
	require.Equal(t, "org_1", provider.authorizationOrganizationID)
	require.Equal(t, "ws_1", provider.authorizationWorkspaceID)
	require.Equal(t, "selected_hosted_models", provider.authorizationParams.AuthorizationMode)
	require.Equal(t, []string{"agentteam/deepseek-v4-pro"}, provider.authorizationParams.SelectedModelRefs)
}

func hostedModelAvailability() availability.Availability {
	return availability.Availability{
		OrganizationID:       "org_1",
		EntitlementStatus:    "eligible_entitled",
		BalanceStatus:        "positive_balance",
		AvailableCreditAtoms: 7500,
		CreditUnit:           "credit_atom",
		UpdatedAt:            time.Date(2026, 6, 6, 1, 2, 3, 0, time.UTC),
		Models: []availability.Model{{
			HostedModelID:         "hm_deepseek_v4_pro",
			ModelReference:        "agentteam/deepseek-v4-pro",
			DisplayName:           "DeepSeek V4 Pro",
			InventoryStatus:       "active_inventory",
			EntitlementStatus:     "eligible_entitled",
			AccessSource:          "default_open",
			BalanceStatus:         "positive_balance",
			RuntimeStatus:         "runtime_supported",
			Enabled:               true,
			PricingFormulaVersion: "hosted_token_context_threshold_v1",
			BillableDimensions:    []string{"input_tokens", "cache_read_input_tokens", "output_tokens"},
			ContextTiers: []availability.PricingTier{{
				ID:               "short_context",
				MinContextTokens: 0,
				MaxContextTokens: int64Ptr(4096),
				Rates: availability.PricingTierRates{
					InputTokensCreditAtomsPerMillionTokens:    100,
					CacheReadInputCreditAtomsPerMillionTokens: 40,
					OutputCreditAtomsPerMillionTokens:         200,
				},
			}},
			SupportedFamilies:                         []string{"messages", "responses"},
			ExecutableWireAPIFamilies:                 []string{"responses"},
			MessagesFeatures:                          []string{"tool_use"},
			ChatCompletionsFeatures:                   []string{"json_object"},
			ResponsesFeatures:                         []string{"text_format"},
			InputCreditAtomsPerMillionTokens:          int64Ptr(100),
			CacheReadInputCreditAtomsPerMillionTokens: int64Ptr(40),
			OutputCreditAtomsPerMillionTokens:         int64Ptr(200),
		}},
	}
}

func hostedModelAuthenticatedRequest(t *testing.T, target string, organizationID string) api.AuthenticatedRequest {
	t.Helper()

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodGet, target, nil)
	ctx.Params = gin.Params{{Key: "orgId", Value: organizationID}}
	return api.AuthenticatedRequest{
		Gin:  ctx,
		User: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
	}
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func derefBool(value *bool) bool {
	return value != nil && *value
}

func derefStringSlice(value *[]string) []string {
	if value == nil {
		return nil
	}
	return *value
}
