// Copyright 2025 Cloudbase Solutions SRL
//
//    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
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
//    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
//    License for the specific language governing permissions and limitations
//    under the License.

package sql

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"testing"

	"github.com/stretchr/testify/suite"

	runnerErrors "github.com/cloudbase/garm-provider-common/errors"
	dbCommon "github.com/cloudbase/garm/database/common"
	"github.com/cloudbase/garm/database/watcher"
	garmTesting "github.com/cloudbase/garm/internal/testing"
	"github.com/cloudbase/garm/params"
)

type FileStoreTestFixtures struct {
	FileObjects []params.FileObject
}

type FileStoreTestSuite struct {
	suite.Suite
	Store    dbCommon.Store
	ctx      context.Context
	adminCtx context.Context
	Fixtures *FileStoreTestFixtures
}

func (s *FileStoreTestSuite) TearDownTest() {
	watcher.CloseWatcher()
}

func (s *FileStoreTestSuite) SetupTest() {
	ctx := context.Background()
	watcher.InitWatcher(ctx)

	db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T()))
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
	}
	s.Store = db

	adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T())
	s.adminCtx = adminCtx
	s.ctx = adminCtx

	// Create test file objects
	fileObjects := []params.FileObject{}

	// File 1: Small text file with tags
	content1 := []byte("Hello, World! This is test file 1.")
	param := params.CreateFileObjectParams{
		Name: "test-file-1.txt",
		Size: int64(len(content1)),
		Tags: []string{"test", "text"},
	}
	fileObj1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create test file 1: %s", err))
	}
	fileObjects = append(fileObjects, fileObj1)

	// File 2: Binary-like content with different tags
	content2 := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00} // PNG header-like
	param = params.CreateFileObjectParams{
		Name: "test-image.png",
		Size: int64(len(content2)),
		Tags: []string{"image", "binary"},
	}
	fileObj2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create test file 2: %s", err))
	}
	fileObjects = append(fileObjects, fileObj2)

	// File 3: No tags
	content3 := []byte("File without tags.")
	param = params.CreateFileObjectParams{
		Name: "no-tags.txt",
		Size: int64(len(content3)),
		Tags: []string{},
	}
	fileObj3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create test file 3: %s", err))
	}
	fileObjects = append(fileObjects, fileObj3)

	s.Fixtures = &FileStoreTestFixtures{
		FileObjects: fileObjects,
	}
}

func (s *FileStoreTestSuite) TestCreateFileObject() {
	content := []byte("New test file content")
	tags := []string{"new", "test"}

	param := params.CreateFileObjectParams{
		Name: "new-file.txt",
		Size: int64(len(content)),
		Tags: tags,
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)
	s.Require().NotZero(fileObj.ID)
	s.Require().Equal("new-file.txt", fileObj.Name)
	s.Require().Equal(int64(len(content)), fileObj.Size)
	s.Require().ElementsMatch(tags, fileObj.Tags)
	s.Require().NotEmpty(fileObj.SHA256)
	s.Require().NotEmpty(fileObj.FileType)

	// Verify SHA256 is correct
	expectedHash := sha256.Sum256(content)
	expectedHashStr := hex.EncodeToString(expectedHash[:])
	s.Require().Equal(expectedHashStr, fileObj.SHA256)
}

func (s *FileStoreTestSuite) TestCreateFileObjectEmpty() {
	content := []byte{}
	param := params.CreateFileObjectParams{
		Name: "empty-file.txt",
		Size: 0,
		Tags: []string{},
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)
	s.Require().NotZero(fileObj.ID)
	s.Require().Equal("empty-file.txt", fileObj.Name)
	s.Require().Equal(int64(0), fileObj.Size)
}

func (s *FileStoreTestSuite) TestGetFileObject() {
	fileObj, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID)
	s.Require().Nil(err)
	s.Require().Equal(s.Fixtures.FileObjects[0].ID, fileObj.ID)
	s.Require().Equal(s.Fixtures.FileObjects[0].Name, fileObj.Name)
	s.Require().Equal(s.Fixtures.FileObjects[0].Size, fileObj.Size)
	s.Require().Equal(s.Fixtures.FileObjects[0].SHA256, fileObj.SHA256)
	s.Require().ElementsMatch(s.Fixtures.FileObjects[0].Tags, fileObj.Tags)
}

func (s *FileStoreTestSuite) TestGetFileObjectNotFound() {
	_, err := s.Store.GetFileObject(s.ctx, 99999)
	s.Require().NotNil(err)
	s.Require().ErrorIs(err, runnerErrors.ErrNotFound)
}

func (s *FileStoreTestSuite) TestOpenFileObjectContent() {
	// Create a file with known content
	content := []byte("Test content for reading")
	param := params.CreateFileObjectParams{
		Name: "read-test.txt",
		Size: int64(len(content)),
		Tags: []string{"read"},
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)

	// Open and read the content
	reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID)
	s.Require().Nil(err)
	s.Require().NotNil(reader)
	defer reader.Close()

	readContent, err := io.ReadAll(reader)
	s.Require().Nil(err)
	s.Require().Equal(content, readContent)
}

func (s *FileStoreTestSuite) TestOpenFileObjectContentNotFound() {
	_, err := s.Store.OpenFileObjectContent(s.ctx, 99999)
	s.Require().NotNil(err)
}

func (s *FileStoreTestSuite) TestListFileObjects() {
	result, err := s.Store.ListFileObjects(s.ctx, 1, 10)
	s.Require().Nil(err)
	s.Require().GreaterOrEqual(len(result.Results), len(s.Fixtures.FileObjects))
	s.Require().Equal(uint64(1), result.CurrentPage)
	s.Require().GreaterOrEqual(result.Pages, uint64(1))
	s.Require().GreaterOrEqual(result.TotalCount, uint64(len(s.Fixtures.FileObjects)))

	// First page should not have previous page
	s.Require().Nil(result.PreviousPage)

	// If there are more pages, next page should be set
	if result.Pages > 1 {
		s.Require().NotNil(result.NextPage)
		s.Require().Equal(uint64(2), *result.NextPage)
	}
}

func (s *FileStoreTestSuite) TestListFileObjectsPagination() {
	// Create more files to test pagination
	for i := 0; i < 5; i++ {
		content := []byte(fmt.Sprintf("File %d", i))
		param := params.CreateFileObjectParams{
			Name: fmt.Sprintf("page-test-%d.txt", i),
			Size: int64(len(content)),
			Tags: []string{"pagination"},
		}
		_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
		s.Require().Nil(err)
	}

	// Test first page with page size of 2
	page1, err := s.Store.ListFileObjects(s.ctx, 1, 2)
	s.Require().Nil(err)
	s.Require().Equal(2, len(page1.Results))
	s.Require().Equal(uint64(1), page1.CurrentPage)
	s.Require().GreaterOrEqual(page1.TotalCount, uint64(5))
	s.Require().Nil(page1.PreviousPage, "First page should not have previous page")
	s.Require().NotNil(page1.NextPage, "First page should have next page")
	s.Require().Equal(uint64(2), *page1.NextPage)

	// Test second page
	page2, err := s.Store.ListFileObjects(s.ctx, 2, 2)
	s.Require().Nil(err)
	s.Require().Equal(2, len(page2.Results))
	s.Require().Equal(uint64(2), page2.CurrentPage)
	s.Require().Equal(page1.Pages, page2.Pages)
	s.Require().Equal(page1.TotalCount, page2.TotalCount)
	s.Require().NotNil(page2.PreviousPage, "Second page should have previous page")
	s.Require().Equal(uint64(1), *page2.PreviousPage)
	s.Require().NotNil(page2.NextPage, "Second page should have next page")
	s.Require().Equal(uint64(3), *page2.NextPage)

	// Verify different results on different pages
	if len(page1.Results) > 0 && len(page2.Results) > 0 {
		page1File := page1.Results[0]
		page2File := page2.Results[0]
		s.Require().NotEqual(page1File.ID, page2File.ID)
	}
}

func (s *FileStoreTestSuite) TestListFileObjectsDefaultPagination() {
	// Test default values (page 0 should become 1, pageSize 0 should become 20)
	result, err := s.Store.ListFileObjects(s.ctx, 0, 0)
	s.Require().Nil(err)
	s.Require().Equal(uint64(1), result.CurrentPage)
	s.Require().LessOrEqual(len(result.Results), 20)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectName() {
	newName := "updated-name.txt"
	updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{
		Name: &newName,
	})
	s.Require().Nil(err)
	s.Require().Equal(newName, updated.Name)
	s.Require().Equal(s.Fixtures.FileObjects[0].ID, updated.ID)
	s.Require().Equal(s.Fixtures.FileObjects[0].Size, updated.Size)     // Size should not change
	s.Require().Equal(s.Fixtures.FileObjects[0].SHA256, updated.SHA256) // SHA256 should not change

	// Verify the change persists
	retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID)
	s.Require().Nil(err)
	s.Require().Equal(newName, retrieved.Name)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectTags() {
	newTags := []string{"updated", "tags", "here"}
	updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{
		Tags: newTags,
	})
	s.Require().Nil(err)
	s.Require().ElementsMatch(newTags, updated.Tags)
	s.Require().Equal(s.Fixtures.FileObjects[0].Name, updated.Name) // Name should not change

	// Verify the change persists
	retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID)
	s.Require().Nil(err)
	s.Require().ElementsMatch(newTags, retrieved.Tags)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectNameAndTags() {
	newName := "completely-updated.txt"
	newTags := []string{"both", "changed"}

	updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{
		Name: &newName,
		Tags: newTags,
	})
	s.Require().Nil(err)
	s.Require().Equal(newName, updated.Name)
	s.Require().ElementsMatch(newTags, updated.Tags)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectEmptyTags() {
	// Test clearing all tags
	emptyTags := []string{}
	updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{
		Tags: emptyTags,
	})
	s.Require().Nil(err)
	s.Require().Empty(updated.Tags)

	// Verify the change persists
	retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID)
	s.Require().Nil(err)
	s.Require().Empty(retrieved.Tags)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectNoChanges() {
	// Update with no changes
	updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{})
	s.Require().Nil(err)
	s.Require().Equal(s.Fixtures.FileObjects[0].Name, updated.Name)
	s.Require().ElementsMatch(s.Fixtures.FileObjects[0].Tags, updated.Tags)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectNotFound() {
	newName := "does-not-exist.txt"
	_, err := s.Store.UpdateFileObject(s.ctx, 99999, params.UpdateFileObjectParams{
		Name: &newName,
	})
	s.Require().NotNil(err)
	s.Require().ErrorIs(err, runnerErrors.ErrNotFound)
}

func (s *FileStoreTestSuite) TestUpdateFileObjectEmptyName() {
	emptyName := ""
	_, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{
		Name: &emptyName,
	})
	s.Require().NotNil(err)
	s.Require().Contains(err.Error(), "name cannot be empty")
}

func (s *FileStoreTestSuite) TestDeleteFileObject() {
	// Create a file to delete
	content := []byte("To be deleted")
	param := params.CreateFileObjectParams{
		Name: "delete-me.txt",
		Size: int64(len(content)),
		Tags: []string{"delete"},
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)

	// Delete the file
	err = s.Store.DeleteFileObject(s.ctx, fileObj.ID)
	s.Require().Nil(err)

	// Verify it's deleted
	_, err = s.Store.GetFileObject(s.ctx, fileObj.ID)
	s.Require().NotNil(err)
	s.Require().ErrorIs(err, runnerErrors.ErrNotFound)
}

func (s *FileStoreTestSuite) TestDeleteFileObjectNotFound() {
	// Deleting non-existent file should not error
	err := s.Store.DeleteFileObject(s.ctx, 99999)
	s.Require().Nil(err)
}

func (s *FileStoreTestSuite) TestCreateFileObjectLargeContent() {
	// Test with larger content (1MB)
	size := 1024 * 1024
	content := make([]byte, size)
	for i := range content {
		content[i] = byte(i % 256)
	}
	param := params.CreateFileObjectParams{
		Name: "large-file.bin",
		Size: int64(size),
		Tags: []string{"large", "binary"},
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)
	s.Require().Equal(int64(size), fileObj.Size)

	// Verify we can read it back
	reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID)
	s.Require().Nil(err)
	defer reader.Close()

	readContent, err := io.ReadAll(reader)
	s.Require().Nil(err)
	s.Require().Equal(content, readContent)
}

func (s *FileStoreTestSuite) TestFileObjectImmutableFields() {
	// Create a file
	content := []byte("Immutable test content")
	param := params.CreateFileObjectParams{
		Name: "immutable-test.txt",
		Size: int64(len(content)),
		Tags: []string{"original"},
	}
	fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)

	originalSize := fileObj.Size
	originalSHA256 := fileObj.SHA256
	originalFileType := fileObj.FileType

	// Update name and tags
	newName := "updated-immutable-test.txt"
	updated, err := s.Store.UpdateFileObject(s.ctx, fileObj.ID, params.UpdateFileObjectParams{
		Name: &newName,
		Tags: []string{"updated"},
	})
	s.Require().Nil(err)

	// Verify immutable fields haven't changed
	s.Require().Equal(originalSize, updated.Size)
	s.Require().Equal(originalSHA256, updated.SHA256)
	s.Require().Equal(originalFileType, updated.FileType)

	// Verify content hasn't changed
	reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID)
	s.Require().Nil(err)
	defer reader.Close()

	readContent, err := io.ReadAll(reader)
	s.Require().Nil(err)
	s.Require().Equal(content, readContent)
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTags() {
	// Create files with specific tags for searching
	content1 := []byte("File with tag1 and tag2")
	param := params.CreateFileObjectParams{
		Name: "search-file-1.txt",
		Size: int64(len(content1)),
		Tags: []string{"tag1", "tag2"},
	}
	file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	s.Require().Nil(err)

	content2 := []byte("File with tag1, tag2, and tag3")
	param = params.CreateFileObjectParams{
		Name: "search-file-2.txt",
		Size: int64(len(content2)),
		Tags: []string{"tag1", "tag2", "tag3"},
	}
	file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	s.Require().Nil(err)

	content3 := []byte("File with only tag1")
	param = params.CreateFileObjectParams{
		Name: "search-file-3.txt",
		Size: int64(len(content3)),
		Tags: []string{"tag1"},
	}
	file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	s.Require().Nil(err)

	content4 := []byte("File with tag3 only")
	param = params.CreateFileObjectParams{
		Name: "search-file-4.txt",
		Size: int64(len(content4)),
		Tags: []string{"tag3"},
	}
	_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4))
	s.Require().Nil(err)

	// Search for files with tag1 - should return 3 files
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"tag1"}, 1, 10)
	s.Require().Nil(err)
	s.Require().GreaterOrEqual(len(result.Results), 3)

	// Verify the expected files are in the results
	foundIDs := make(map[uint]bool)
	for _, fileObj := range result.Results {
		foundIDs[fileObj.ID] = true
	}
	s.Require().True(foundIDs[file1.ID], "file1 should be in results")
	s.Require().True(foundIDs[file2.ID], "file2 should be in results")
	s.Require().True(foundIDs[file3.ID], "file3 should be in results")
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsMultipleTags() {
	// Create files with various tag combinations
	content1 := []byte("File with search1 and search2")
	param := params.CreateFileObjectParams{
		Name: "multi-search-1.txt",
		Size: int64(len(content1)),
		Tags: []string{"search1", "search2"},
	}
	file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	s.Require().Nil(err)

	content2 := []byte("File with search1, search2, and search3")
	param = params.CreateFileObjectParams{
		Name: "multi-search-2.txt",
		Size: int64(len(content2)),
		Tags: []string{"search1", "search2", "search3"},
	}
	file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	s.Require().Nil(err)

	content3 := []byte("File with only search1")
	param = params.CreateFileObjectParams{
		Name: "multi-search-3.txt",
		Size: int64(len(content3)),
		Tags: []string{"search1"},
	}
	_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	s.Require().Nil(err)

	// Search for files with both search1 AND search2 - should return only 2 files
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"search1", "search2"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(2, len(result.Results))

	// Verify the correct files are returned
	foundIDs := make(map[uint]bool)
	for _, fileObj := range result.Results {
		foundIDs[fileObj.ID] = true
	}
	s.Require().True(foundIDs[file1.ID], "file1 should be in results")
	s.Require().True(foundIDs[file2.ID], "file2 should be in results")
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsNoResults() {
	// Search for a tag that doesn't exist
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"nonexistent-tag"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(0, len(result.Results))
	s.Require().Equal(uint64(0), result.Pages)
	s.Require().Equal(uint64(1), result.CurrentPage)
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsEmptyTags() {
	// Search with empty tag list - should return all files
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{}, 1, 100)
	s.Require().Nil(err)
	// Should return all files (fixtures + any created in other tests)
	s.Require().GreaterOrEqual(len(result.Results), len(s.Fixtures.FileObjects))
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsPagination() {
	// Create multiple files with the same tag
	for i := 0; i < 5; i++ {
		content := []byte(fmt.Sprintf("Pagination test file %d", i))
		param := params.CreateFileObjectParams{
			Name: fmt.Sprintf("page-search-%d.txt", i),
			Size: int64(len(content)),
			Tags: []string{"pagination-test"},
		}
		_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
		s.Require().Nil(err)
	}

	// Test first page with page size of 2
	page1, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"pagination-test"}, 1, 2)
	s.Require().Nil(err)
	s.Require().Equal(2, len(page1.Results))
	s.Require().Equal(uint64(1), page1.CurrentPage)
	s.Require().GreaterOrEqual(page1.Pages, uint64(3)) // At least 3 pages for 5 files

	// Test second page
	page2, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"pagination-test"}, 2, 2)
	s.Require().Nil(err)
	s.Require().Equal(2, len(page2.Results))
	s.Require().Equal(uint64(2), page2.CurrentPage)

	// Verify different results on different pages
	if len(page1.Results) > 0 && len(page2.Results) > 0 {
		page1File := page1.Results[0]
		page2File := page2.Results[0]
		s.Require().NotEqual(page1File.ID, page2File.ID)
	}
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsDefaultPagination() {
	// Create a file with a unique tag
	content := []byte("Default pagination test")
	param := params.CreateFileObjectParams{
		Name: "default-page-search.txt",
		Size: int64(len(content)),
		Tags: []string{"default-pagination"},
	}
	_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)

	// Test default values (page 0 should become 1, pageSize 0 should become 20)
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"default-pagination"}, 0, 0)
	s.Require().Nil(err)
	s.Require().Equal(uint64(1), result.CurrentPage)
	s.Require().LessOrEqual(len(result.Results), 20)
	s.Require().GreaterOrEqual(len(result.Results), 1)
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsAllTagsRequired() {
	// Test that search requires ALL specified tags (AND logic, not OR)
	content1 := []byte("Has A and B")
	param := params.CreateFileObjectParams{
		Name: "and-test-1.txt",
		Size: int64(len(content1)),
		Tags: []string{"tagA", "tagB"},
	}
	file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	s.Require().Nil(err)

	content2 := []byte("Has A, B, and C")
	param = params.CreateFileObjectParams{
		Name: "and-test-2.txt",
		Size: int64(len(content2)),
		Tags: []string{"tagA", "tagB", "tagC"},
	}
	file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	s.Require().Nil(err)

	content3 := []byte("Has only A")
	param = params.CreateFileObjectParams{
		Name: "and-test-3.txt",
		Size: int64(len(content3)),
		Tags: []string{"tagA"},
	}
	_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	s.Require().Nil(err)

	content4 := []byte("Has only B")
	param = params.CreateFileObjectParams{
		Name: "and-test-4.txt",
		Size: int64(len(content4)),
		Tags: []string{"tagB"},
	}
	_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4))
	s.Require().Nil(err)

	// Search for files with BOTH tagA AND tagB
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"tagA", "tagB"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(2, len(result.Results), "Should only return files with BOTH tags")

	// Verify the correct files are returned
	foundIDs := make(map[uint]bool)
	for _, fileObj := range result.Results {
		foundIDs[fileObj.ID] = true
		// Verify each result has both tags
		s.Require().Contains(fileObj.Tags, "tagA")
		s.Require().Contains(fileObj.Tags, "tagB")
	}
	s.Require().True(foundIDs[file1.ID])
	s.Require().True(foundIDs[file2.ID])
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsCaseInsensitive() {
	// Test case insensitivity of tag search (COLLATE NOCASE)
	content1 := []byte("File with lowercase tag")
	param := params.CreateFileObjectParams{
		Name: "case-test-1.txt",
		Size: int64(len(content1)),
		Tags: []string{"TestTag"},
	}
	file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	s.Require().Nil(err)

	content2 := []byte("File with UPPERCASE tag")
	param = params.CreateFileObjectParams{
		Name: "case-test-2.txt",
		Size: int64(len(content2)),
		Tags: []string{"TESTTAG"},
	}
	file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	s.Require().Nil(err)

	content3 := []byte("File with MixedCase tag")
	param = params.CreateFileObjectParams{
		Name: "case-test-3.txt",
		Size: int64(len(content3)),
		Tags: []string{"testTAG"},
	}
	file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	s.Require().Nil(err)

	// Search for lowercase - should return all files (case insensitive)
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"testtag"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(3, len(result.Results), "Should match all case variations")

	foundIDs := make(map[uint]bool)
	for _, fileObj := range result.Results {
		foundIDs[fileObj.ID] = true
	}
	s.Require().True(foundIDs[file1.ID], "Should find file with 'TestTag'")
	s.Require().True(foundIDs[file2.ID], "Should find file with 'TESTTAG'")
	s.Require().True(foundIDs[file3.ID], "Should find file with 'testTAG'")

	// Search for UPPERCASE - should also return all files
	result, err = s.Store.SearchFileObjectByTags(s.ctx, []string{"TESTTAG"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(3, len(result.Results), "Should match all case variations")

	// Search for MixedCase - should also return all files
	result, err = s.Store.SearchFileObjectByTags(s.ctx, []string{"TeStTaG"}, 1, 10)
	s.Require().Nil(err)
	s.Require().Equal(3, len(result.Results), "Should match all case variations")
}

func (s *FileStoreTestSuite) TestSearchFileObjectByTagsOrderByCreatedAt() {
	// Create files with same tag at different times to test ordering
	tag := "order-test"

	content1 := []byte("First file")
	param := params.CreateFileObjectParams{
		Name: "order-1.txt",
		Size: int64(len(content1)),
		Tags: []string{tag},
	}
	file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
	s.Require().Nil(err)

	content2 := []byte("Second file")
	param = params.CreateFileObjectParams{
		Name: "order-2.txt",
		Size: int64(len(content2)),
		Tags: []string{tag},
	}
	file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
	s.Require().Nil(err)

	content3 := []byte("Third file")
	param = params.CreateFileObjectParams{
		Name: "order-3.txt",
		Size: int64(len(content3)),
		Tags: []string{tag},
	}
	file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
	s.Require().Nil(err)

	// Search and verify order (should be DESC by created_at, so newest first)
	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{tag}, 1, 10)
	s.Require().Nil(err)
	s.Require().GreaterOrEqual(len(result.Results), 3)

	// The most recently created files should be first
	// We can at least verify that file3 comes before file1 in the results
	var file1Idx, file3Idx int
	for i, fileObj := range result.Results {
		if fileObj.ID == file1.ID {
			file1Idx = i
		}
		if fileObj.ID == file3.ID {
			file3Idx = i
		}
	}
	s.Require().Less(file3Idx, file1Idx, "Newer file (file3) should appear before older file (file1)")

	// Also verify file2 comes before file1
	var file2Idx int
	for i, fileObj := range result.Results {
		if fileObj.ID == file2.ID {
			file2Idx = i
		}
	}
	s.Require().Less(file2Idx, file1Idx, "Newer file (file2) should appear before older file (file1)")
}

func (s *FileStoreTestSuite) TestPaginationFieldsLastPage() {
	// Create exactly 5 files
	for i := 0; i < 5; i++ {
		content := []byte(fmt.Sprintf("Last page test %d", i))
		param := params.CreateFileObjectParams{
			Name: fmt.Sprintf("last-page-test-%d.txt", i),
			Size: int64(len(content)),
			Tags: []string{"last-page"},
		}
		_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
		s.Require().Nil(err)
	}

	// Get the last page (should have 1 item with pageSize=2)
	// Total: 5 items, pageSize: 2, so pages: 3 (2, 2, 1)
	lastPage, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"last-page"}, 3, 2)
	s.Require().Nil(err)
	s.Require().Equal(uint64(3), lastPage.CurrentPage)
	s.Require().Equal(uint64(3), lastPage.Pages)
	s.Require().Equal(uint64(5), lastPage.TotalCount)
	s.Require().Equal(1, len(lastPage.Results), "Last page should have 1 item")
	s.Require().NotNil(lastPage.PreviousPage, "Last page should have previous page")
	s.Require().Equal(uint64(2), *lastPage.PreviousPage)
	s.Require().Nil(lastPage.NextPage, "Last page should not have next page")
}

func (s *FileStoreTestSuite) TestPaginationFieldsSinglePage() {
	// Test when all results fit in a single page
	content := []byte("Single page test")
	param := params.CreateFileObjectParams{
		Name: "single-page-test.txt",
		Size: int64(len(content)),
		Tags: []string{"single-page-unique-tag"},
	}
	_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
	s.Require().Nil(err)

	result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"single-page-unique-tag"}, 1, 20)
	s.Require().Nil(err)
	s.Require().Equal(uint64(1), result.TotalCount)
	s.Require().Equal(uint64(1), result.Pages)
	s.Require().Equal(uint64(1), result.CurrentPage)
	s.Require().Nil(result.PreviousPage, "Single page should not have previous")
	s.Require().Nil(result.NextPage, "Single page should not have next")
}

func TestFileStoreTestSuite(t *testing.T) {
	suite.Run(t, new(FileStoreTestSuite))
}
