// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package mongo

import (
	"context"
	"testing"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/event"
	"go.mongodb.org/mongo-driver/v2/internal/assert"
	"go.mongodb.org/mongo-driver/v2/internal/integtest"
	"go.mongodb.org/mongo-driver/v2/internal/mongoutil"
	"go.mongodb.org/mongo-driver/v2/internal/require"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/mongo/readpref"
	"go.mongodb.org/mongo-driver/v2/mongo/writeconcern"
)

var gridfsConnsCheckedOut int

func TestGridFS(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	cs := integtest.ConnString(t)
	poolMonitor := &event.PoolMonitor{
		Event: func(evt *event.PoolEvent) {
			switch evt.Type {
			case event.ConnectionCheckedOut:
				gridfsConnsCheckedOut++
			case event.ConnectionCheckedIn:
				gridfsConnsCheckedOut--
			}
		},
	}
	clientOpts := options.Client().
		ApplyURI(cs.Original).
		SetReadPreference(readpref.Primary()).
		SetWriteConcern(writeconcern.Majority()).
		SetPoolMonitor(poolMonitor).
		// Connect to a single host. For sharded clusters, this will pin to a single mongos, which avoids
		// non-deterministic versioning errors in the server. This has no effect for replica sets because the driver
		// will discover the other hosts during SDAM checks.
		SetHosts(cs.Hosts[:1])

	integtest.AddTestServerAPIVersion(clientOpts)

	client, err := Connect(clientOpts)
	assert.Nil(t, err, "Connect error: %v", err)
	db := client.Database("gridfs")
	defer func() {
		sessions := client.NumberSessionsInProgress()
		conns := gridfsConnsCheckedOut

		_ = db.Drop(context.Background())
		_ = client.Disconnect(context.Background())
		assert.Equal(t, 0, sessions, "%v sessions checked out", sessions)
		assert.Equal(t, 0, conns, "%v connections checked out", conns)
	}()

	// Unit tests showing the chunk size is set correctly on the bucket and upload stream objects.
	t.Run("ChunkSize", func(t *testing.T) {
		chunkSizeTests := []struct {
			testName   string
			bucketOpts *options.BucketOptionsBuilder
			uploadOpts *options.GridFSUploadOptionsBuilder
		}{
			{"Default values", nil, nil},
			{"Options provided without chunk size", options.GridFSBucket(), options.GridFSUpload()},
			{"Bucket chunk size set", options.GridFSBucket().SetChunkSizeBytes(27), nil},
			{"Upload stream chunk size set", nil, options.GridFSUpload().SetChunkSizeBytes(27)},
			{"Bucket and upload set to different values", options.GridFSBucket().SetChunkSizeBytes(27), options.GridFSUpload().SetChunkSizeBytes(31)},
		}

		for _, tt := range chunkSizeTests {
			t.Run(tt.testName, func(t *testing.T) {
				bucket := db.GridFSBucket(tt.bucketOpts)

				us, err := bucket.OpenUploadStream(context.Background(), "filename", tt.uploadOpts)
				assert.Nil(t, err, "OpenUploadStream error: %v", err)

				bucketArgs, err := mongoutil.NewOptions[options.BucketOptions](tt.bucketOpts)
				require.NoError(t, err, "failed to construct options from builder")

				expectedBucketChunkSize := DefaultGridFSChunkSize
				if tt.bucketOpts != nil && bucketArgs.ChunkSizeBytes != nil {
					expectedBucketChunkSize = *bucketArgs.ChunkSizeBytes
				}
				assert.Equal(t, expectedBucketChunkSize, bucket.chunkSize,
					"expected chunk size %v, got %v", expectedBucketChunkSize, bucket.chunkSize)

				uploadArgs, err := mongoutil.NewOptions[options.GridFSUploadOptions](tt.uploadOpts)
				require.NoError(t, err, "failed to construct options from builder")

				expectedUploadChunkSize := expectedBucketChunkSize
				if tt.uploadOpts != nil && uploadArgs.ChunkSizeBytes != nil {
					expectedUploadChunkSize = *uploadArgs.ChunkSizeBytes
				}
				assert.Equal(t, expectedUploadChunkSize, us.chunkSize,
					"expected chunk size %v, got %v", expectedUploadChunkSize, us.chunkSize)
			})
		}
	})
}

func TestGridFSFile_UnmarshalBSON(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	cs := integtest.ConnString(t)

	clientOpts := options.Client().
		ApplyURI(cs.Original).
		SetReadPreference(readpref.Primary()).
		SetWriteConcern(writeconcern.Majority()).
		// Connect to a single host. For sharded clusters, this will pin to a single mongos, which avoids
		// non-deterministic versioning errors in the server. This has no effect for replica sets because the driver
		// will discover the other hosts during SDAM checks.
		SetHosts(cs.Hosts[:1])

	integtest.AddTestServerAPIVersion(clientOpts)

	client, err := Connect(clientOpts)
	require.NoError(t, err)

	defer func() {
		err := client.Disconnect(context.Background())
		require.NoError(t, err)
	}()

	// Get the database and create a GridFS bucket
	db := client.Database("gridfs_test_db")

	// Drop the collection
	err = db.Collection("myfiles.files").Drop(context.Background())
	require.NoError(t, err)

	err = db.Collection("myfiles.chunks").Drop(context.Background())
	require.NoError(t, err)

	bucket := db.GridFSBucket(options.GridFSBucket().SetName("myfiles"))

	// Data to upload
	fileName := "example-file.txt"
	fileContent := []byte("Hello GridFS! This is a test file.")

	// Upload data into GridFS
	uploadStream, err := bucket.OpenUploadStream(context.Background(), fileName)
	require.NoError(t, err)

	_, err = uploadStream.Write(fileContent)
	require.NoError(t, err)

	uploadStream.Close()

	// Verify the file metadata
	fileCursor, err := bucket.Find(context.Background(), bson.D{})
	require.NoError(t, err)

	for fileCursor.Next(context.Background()) {
		var file GridFSFile
		err := fileCursor.Decode(&file)
		require.NoError(t, err)

		assert.NotNil(t, file.ID)
		assert.Equal(t, int64(34), file.Length)
		assert.Equal(t, int32(261120), file.ChunkSize)
		assert.NotNil(t, file.UploadDate)
		assert.Equal(t, fileName, file.Name)
	}
}
