//go:build integration

package openai_test

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"os"
	"testing"

	"github.com/sashabaranov/go-openai"
	"github.com/sashabaranov/go-openai/internal/test/checks"
	"github.com/sashabaranov/go-openai/jsonschema"
)

func TestAPI(t *testing.T) {
	apiToken := os.Getenv("OPENAI_TOKEN")
	if apiToken == "" {
		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
	}

	var err error
	c := openai.NewClient(apiToken)
	ctx := context.Background()
	_, err = c.ListEngines(ctx)
	checks.NoError(t, err, "ListEngines error")

	_, err = c.GetEngine(ctx, openai.GPT3Davinci002)
	checks.NoError(t, err, "GetEngine error")

	fileRes, err := c.ListFiles(ctx)
	checks.NoError(t, err, "ListFiles error")

	if len(fileRes.Files) > 0 {
		_, err = c.GetFile(ctx, fileRes.Files[0].ID)
		checks.NoError(t, err, "GetFile error")
	} // else skip

	embeddingReq := openai.EmbeddingRequest{
		Input: []string{
			"The food was delicious and the waiter",
			"Other examples of embedding request",
		},
		Model: openai.AdaEmbeddingV2,
	}
	_, err = c.CreateEmbeddings(ctx, embeddingReq)
	checks.NoError(t, err, "Embedding error")

	_, err = c.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model: openai.GPT3Dot5Turbo,
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleUser,
					Content: "Hello!",
				},
			},
		},
	)

	checks.NoError(t, err, "CreateChatCompletion (without name) returned error")

	_, err = c.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model: openai.GPT3Dot5Turbo,
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleUser,
					Name:    "John_Doe",
					Content: "Hello!",
				},
			},
		},
	)
	checks.NoError(t, err, "CreateChatCompletion (with name) returned error")

	_, err = c.CreateChatCompletion(
		context.Background(),
		openai.ChatCompletionRequest{
			Model: openai.GPT3Dot5Turbo,
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleUser,
					Content: "What is the weather like in Boston?",
				},
			},
			Functions: []openai.FunctionDefinition{{
				Name: "get_current_weather",
				Parameters: jsonschema.Definition{
					Type: jsonschema.Object,
					Properties: map[string]jsonschema.Definition{
						"location": {
							Type:        jsonschema.String,
							Description: "The city and state, e.g. San Francisco, CA",
						},
						"unit": {
							Type: jsonschema.String,
							Enum: []string{"celsius", "fahrenheit"},
						},
					},
					Required: []string{"location"},
				},
			}},
		},
	)
	checks.NoError(t, err, "CreateChatCompletion (with functions) returned error")
}

func TestCompletionStream(t *testing.T) {
	apiToken := os.Getenv("OPENAI_TOKEN")
	if apiToken == "" {
		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
	}

	c := openai.NewClient(apiToken)
	ctx := context.Background()

	stream, err := c.CreateCompletionStream(ctx, openai.CompletionRequest{
		Prompt:    "Ex falso quodlibet",
		Model:     openai.GPT3Babbage002,
		MaxTokens: 5,
		Stream:    true,
	})
	checks.NoError(t, err, "CreateCompletionStream returned error")
	defer stream.Close()

	counter := 0
	for {
		_, err = stream.Recv()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			t.Errorf("Stream error: %v", err)
		} else {
			counter++
		}
	}
	if counter == 0 {
		t.Error("Stream did not return any responses")
	}
}

func TestAPIError(t *testing.T) {
	apiToken := os.Getenv("OPENAI_TOKEN")
	if apiToken == "" {
		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
	}

	var err error
	c := openai.NewClient(apiToken + "_invalid")
	ctx := context.Background()
	_, err = c.ListEngines(ctx)
	checks.HasError(t, err, "ListEngines should fail with an invalid key")

	var apiErr *openai.APIError
	if !errors.As(err, &apiErr) {
		t.Fatalf("Error is not an APIError: %+v", err)
	}

	if apiErr.HTTPStatusCode != 401 {
		t.Fatalf("Unexpected API error status code: %d", apiErr.HTTPStatusCode)
	}

	switch v := apiErr.Code.(type) {
	case string:
		if v != "invalid_api_key" {
			t.Fatalf("Unexpected API error code: %s", v)
		}
	default:
		t.Fatalf("Unexpected API error code type: %T", v)
	}

	if apiErr.Error() == "" {
		t.Fatal("Empty error message occurred")
	}
}

func TestChatCompletionResponseFormat_JSONSchema(t *testing.T) {
	apiToken := os.Getenv("OPENAI_TOKEN")
	if apiToken == "" {
		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
	}

	var err error
	c := openai.NewClient(apiToken)
	ctx := context.Background()

	type MyStructuredResponse struct {
		PascalCase string `json:"pascal_case" required:"true" description:"PascalCase"`
		CamelCase  string `json:"camel_case" required:"true" description:"CamelCase"`
		KebabCase  string `json:"kebab_case" required:"true" description:"KebabCase"`
		SnakeCase  string `json:"snake_case" required:"true" description:"SnakeCase"`
	}
	var result MyStructuredResponse
	schema, err := jsonschema.GenerateSchemaForType(result)
	if err != nil {
		t.Fatal("CreateChatCompletion (use json_schema response) GenerateSchemaForType error")
	}
	resp, err := c.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model: openai.GPT4oMini,
			Messages: []openai.ChatCompletionMessage{
				{
					Role: openai.ChatMessageRoleSystem,
					Content: "Please enter a string, and we will convert it into the following naming conventions:" +
						"1. PascalCase: Each word starts with an uppercase letter, with no spaces or separators." +
						"2. CamelCase: The first word starts with a lowercase letter, " +
						"and subsequent words start with an uppercase letter, with no spaces or separators." +
						"3. KebabCase: All letters are lowercase, with words separated by hyphens `-`." +
						"4. SnakeCase: All letters are lowercase, with words separated by underscores `_`.",
				},
				{
					Role:    openai.ChatMessageRoleUser,
					Content: "Hello World",
				},
			},
			ResponseFormat: &openai.ChatCompletionResponseFormat{
				Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
				JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
					Name:   "cases",
					Schema: schema,
					Strict: true,
				},
			},
		},
	)
	checks.NoError(t, err, "CreateChatCompletion (use json_schema response) returned error")
	if err == nil {
		err = schema.Unmarshal(resp.Choices[0].Message.Content, &result)
		checks.NoError(t, err, "CreateChatCompletion (use json_schema response) unmarshal error")
	}
}

func TestChatCompletionStructuredOutputsFunctionCalling(t *testing.T) {
	apiToken := os.Getenv("OPENAI_TOKEN")
	if apiToken == "" {
		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
	}

	var err error
	c := openai.NewClient(apiToken)
	ctx := context.Background()

	resp, err := c.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model: openai.GPT4oMini,
			Messages: []openai.ChatCompletionMessage{
				{
					Role: openai.ChatMessageRoleSystem,
					Content: "Please enter a string, and we will convert it into the following naming conventions:" +
						"1. PascalCase: Each word starts with an uppercase letter, with no spaces or separators." +
						"2. CamelCase: The first word starts with a lowercase letter, " +
						"and subsequent words start with an uppercase letter, with no spaces or separators." +
						"3. KebabCase: All letters are lowercase, with words separated by hyphens `-`." +
						"4. SnakeCase: All letters are lowercase, with words separated by underscores `_`.",
				},
				{
					Role:    openai.ChatMessageRoleUser,
					Content: "Hello World",
				},
			},
			Tools: []openai.Tool{
				{
					Type: openai.ToolTypeFunction,
					Function: &openai.FunctionDefinition{
						Name:   "display_cases",
						Strict: true,
						Parameters: &jsonschema.Definition{
							Type: jsonschema.Object,
							Properties: map[string]jsonschema.Definition{
								"PascalCase": {
									Type: jsonschema.String,
								},
								"CamelCase": {
									Type: jsonschema.String,
								},
								"KebabCase": {
									Type: jsonschema.String,
								},
								"SnakeCase": {
									Type: jsonschema.String,
								},
							},
							Required:             []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"},
							AdditionalProperties: false,
						},
					},
				},
			},
			ToolChoice: openai.ToolChoice{
				Type: openai.ToolTypeFunction,
				Function: openai.ToolFunction{
					Name: "display_cases",
				},
			},
		},
	)
	checks.NoError(t, err, "CreateChatCompletion (use structured outputs response) returned error")
	var result = make(map[string]string)
	err = json.Unmarshal([]byte(resp.Choices[0].Message.ToolCalls[0].Function.Arguments), &result)
	checks.NoError(t, err, "CreateChatCompletion (use structured outputs response) unmarshal error")
	for _, key := range []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"} {
		if _, ok := result[key]; !ok {
			t.Errorf("key:%s does not exist.", key)
		}
	}
}
