package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"bookhoard/internal/database"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPreviewCollection(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	libraryID := setup.CreateLibrary(t, "Preview Test Library", "ebooks")
	bookIDs := createPreviewTestMediaItems(t, setup.DB, libraryID, 5)

	t.Run("NoAuth_ReturnsUnauthorized", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id":      libraryID,
			"rules":           []map[string]interface{}{},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusUnauthorized, rec.Code)
	})

	t.Run("MissingLibraryID_ReturnsBadRequest", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id":      "",
			"rules":           []map[string]interface{}{},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
	})

	t.Run("InvalidLibraryID_ReturnsBadRequest", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id":      "not-a-valid-uuid",
			"rules":           []map[string]interface{}{},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
	})

	t.Run("NonExistentLibrary_ReturnsBadRequest", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id":      uuid.New().String(),
			"rules":           []map[string]interface{}{},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
	})

	t.Run("EmptyRules_ReturnsEmptyItems", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id":      libraryID,
			"rules":           []map[string]interface{}{},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 0, len(items), "Empty rules should return no matched items")
	})

	t.Run("ManualBookSelection_ReturnsSelectedBooks", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				bookIDs[0],
				bookIDs[2],
			},
			"limit": 20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 2, len(items), "Should return exactly 2 manually selected books")
	})

	t.Run("RuleFiltering_ReturnsMatchingBooks", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules": []map[string]interface{}{
				{
					"id":       "rule-1",
					"field":    "genre",
					"operator": "equals",
					"value":    "Science Fiction",
					"priority": 5,
				},
			},
			"manual_book_ids": []string{},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Greater(t, len(items), 0, "Should return books matching the genre rule")
	})

	t.Run("CombinedRulesAndManualSelection_ReturnsAllMatches", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules": []map[string]interface{}{
				{
					"id":       "rule-1",
					"field":    "genre",
					"operator": "equals",
					"value":    "Science Fiction",
					"priority": 5,
				},
			},
			"manual_book_ids": []string{bookIDs[4]},
			"limit":           20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Greater(t, len(items), 0, "Should return books from rules and manual selection")
	})

	t.Run("LimitRespected_ReturnsLimitedItems", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				bookIDs[0],
				bookIDs[1],
				bookIDs[2],
				bookIDs[3],
				bookIDs[4],
			},
			"limit": 2,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.LessOrEqual(t, len(items), 2, "Should respect limit parameter")
	})

	t.Run("LimitExceedsMax_DefaultsToTwenty", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				bookIDs[0],
			},
			"limit": 150,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 1, len(items), "Should still return items when limit exceeds max")
	})

	t.Run("LimitZero_DefaultsToTwenty", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				bookIDs[0],
			},
			"limit": 0,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 1, len(items), "Limit 0 should default to 20 and still return matched items")
	})

	t.Run("InvalidManualBookID_SkippedGracefully", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				"not-a-valid-uuid",
				bookIDs[0],
			},
			"limit": 20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 1, len(items), "Invalid book IDs should be skipped, valid ones included")
	})

	t.Run("DuplicateManualBookIDs_ReturnsUniqueItems", func(t *testing.T) {
		reqBody := map[string]interface{}{
			"library_id": libraryID,
			"rules":      []map[string]interface{}{},
			"manual_book_ids": []string{
				bookIDs[0],
				bookIDs[0],
				bookIDs[0],
			},
			"limit": 20,
		}
		body, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		var result map[string]interface{}
		err := json.NewDecoder(rec.Body).Decode(&result)
		require.NoError(t, err)

		items := result["items"].([]interface{})
		assert.Equal(t, 1, len(items), "Duplicate book IDs should result in unique items")
	})

	t.Run("InvalidJSON_ReturnsBadRequest", func(t *testing.T) {
		req := httptest.NewRequest("POST", "/api/collections/preview", bytes.NewBuffer([]byte("invalid json")))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+setup.UserToken)
		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
	})
}

func createPreviewTestMediaItems(t *testing.T, db *database.Queries, libraryID string, count int) []string {
	ctx := context.Background()

	libUUID, err := uuid.Parse(libraryID)
	require.NoError(t, err, "Should parse library ID")

	var bookIDs []string

	genres := []string{"Science Fiction", "Fantasy", "Mystery", "Science Fiction", "Romance"}

	for i := 0; i < count; i++ {
		genre := genres[i%len(genres)]

		media, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
			LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
			Title:     "Preview Test Book " + string(rune('A'+i)),
			Author:    pgtype.Text{String: "Test Author " + string(rune('A'+i)), Valid: true},
			Genre:     pgtype.Text{String: genre, Valid: true},
			FilePath:  "/tmp/test" + string(rune('A'+i)) + ".epub",
			FileSize:  pgtype.Int8{Int64: 1024, Valid: true},
			MimeType:  pgtype.Text{String: "application/epub+zip", Valid: true},
		})
		require.NoError(t, err, "Should create media item")

		mediaUUID, err := uuid.FromBytes(media.ID.Bytes[0:16])
		require.NoError(t, err, "Should parse media UUID")

		bookIDs = append(bookIDs, mediaUUID.String())
	}

	return bookIDs
}
