//go:build test
// +build test

// Package portfolio contains tests for portfolio image management endpoints.
//
// Test Coverage:
//   - ListImages: GET /api/portfolio/images - List all images, optionally filter by tag
//   - ListTags: GET /api/portfolio/tags - List all unique tags, optionally search
//   - ListFilters: GET /api/portfolio/filters - List available filter categories
//   - GetImage: GET /api/portfolio/images/{id} - Get single image details by timestamp ID
//   - UploadImage: POST /api/portfolio/images - Upload new image (admin only)
//   - DeleteImage: DELETE /api/portfolio/images/{id} - Delete image (admin only)
//
// Authentication: Upload/Delete require admin role (403 for non-admins, 401 for unauth).
// Note: Upload/Delete tests verify auth only; S3 operations not fully tested (requires mock).
package portfolio

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

	"crussell/db"
	"crussell/mw"
	"crussell/testutils/jwt"
	"crussell/testutils/testdb"

	"github.com/go-chi/chi/v5"
	"github.com/kovidgoyal/imaging"
)

func TestMain(m *testing.M) {
	pool, err := testdb.NewPool("")
	if err != nil {
		os.Exit(1)
	}
	testdb.Migrate(&testing.T{}, pool)
	db.DB = pool
	jwt.Init()
	code := m.Run()
	pool.Close()
	os.Exit(code)
}

func resetTestData(t *testing.T) {
	t.Helper()
	testdb.TruncateTables(t, db.DB)
}

func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

func makeRequestWithContext(handler http.HandlerFunc, method, path string, body interface{}, userID, role string) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}

	// Add user context
	ctx := req.Context()
	ctx = context.WithValue(ctx, mw.UserIDKey, userID)
	ctx = context.WithValue(ctx, mw.UserRoleKey, role)
	req = req.WithContext(ctx)

	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

// =============================================================================
// List Images Tests
// =============================================================================

// TestPortfolio_ListImages verifies that listing portfolio images returns all images in the database.
func TestPortfolio_ListImages(t *testing.T) {
	resetTestData(t)

	// Insert test images
	_, err := db.DB.Exec(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES
			('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest', 'color:green']),
			('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean', 'color:blue'])
	`)
	if err != nil {
		t.Fatalf("failed to create images: %v", err)
	}

	handler := http.HandlerFunc(ListImages)
	w := makeRequest(handler, "GET", "/api/portfolio/images", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var images []Image
	if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(images) != 2 {
		t.Errorf("expected 2 images, got %d", len(images))
	}
}

// TestPortfolio_ListImages_WithTagFilter verifies that images can be filtered by tag using the 'tag' query parameter.
func TestPortfolio_ListImages_WithTagFilter(t *testing.T) {
	resetTestData(t)

	// Insert test images
	_, err := db.DB.Exec(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES
			('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest']),
			('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean'])
	`)
	if err != nil {
		t.Fatalf("failed to create images: %v", err)
	}

	handler := http.HandlerFunc(ListImages)
	w := makeRequest(handler, "GET", "/api/portfolio/images?tag=forest", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var images []Image
	if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(images) != 1 {
		t.Errorf("expected 1 image, got %d", len(images))
	}
}

// TestPortfolio_ListImages_Empty verifies that an empty database returns an empty images array (not an error).
func TestPortfolio_ListImages_Empty(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(ListImages)
	w := makeRequest(handler, "GET", "/api/portfolio/images", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var images []Image
	if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(images) != 0 {
		t.Errorf("expected 0 images, got %d", len(images))
	}
}

// =============================================================================
// List Tags Tests
// =============================================================================

// TestPortfolio_ListTags verifies that listing tags returns all unique tags from portfolio images.
func TestPortfolio_ListTags(t *testing.T) {
	resetTestData(t)

	// Insert test images with tag_names instead of directly into tags table
	_, err := db.DB.Exec(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES
			('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest']),
			('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean']),
			('https://example.com/img3.jpg', 'https://example.com/img3_thumb.jpg', ARRAY['color:green'])
	`)
	if err != nil {
		t.Fatalf("failed to create images: %v", err)
	}

	handler := http.HandlerFunc(ListTags)
	w := makeRequest(handler, "GET", "/api/portfolio/tags", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var tags []Tag
	if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(tags) != 3 {
		t.Errorf("expected 3 tags, got %d", len(tags))
	}
}

// TestPortfolio_ListTags_WithQuery verifies that tags can be filtered by a query string.
func TestPortfolio_ListTags_WithQuery(t *testing.T) {
	resetTestData(t)

	// Insert test images with tag_names instead of directly into tags table
	_, err := db.DB.Exec(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES
			('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest']),
			('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean']),
			('https://example.com/img3.jpg', 'https://example.com/img3_thumb.jpg', ARRAY['color:green'])
	`)
	if err != nil {
		t.Fatalf("failed to create images: %v", err)
	}

	handler := http.HandlerFunc(ListTags)
	w := makeRequest(handler, "GET", "/api/portfolio/tags?q=forest", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var tags []Tag
	if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(tags) != 1 {
		t.Errorf("expected 1 tag, got %d", len(tags))
	}
}

// TestPortfolio_ListTags_Empty verifies that an empty database returns an empty tags array.
func TestPortfolio_ListTags_Empty(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(ListTags)
	w := makeRequest(handler, "GET", "/api/portfolio/tags", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var tags []Tag
	if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(tags) != 0 {
		t.Errorf("expected 0 tags, got %d", len(tags))
	}
}

// =============================================================================
// List Filters Tests
// =============================================================================

// TestPortfolio_ListFilters verifies that filter categories are derived from tags (e.g., 'nature', 'color' from 'nature:forest').
func TestPortfolio_ListFilters(t *testing.T) {
	resetTestData(t)

	// Insert test images with tags
	_, err := db.DB.Exec(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES
			('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest', 'color:green']),
			('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean', 'color:blue'])
	`)
	if err != nil {
		t.Fatalf("failed to create images: %v", err)
	}

	handler := http.HandlerFunc(ListFilters)
	w := makeRequest(handler, "GET", "/api/portfolio/filters", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var filters []FilterCategory
	if err := json.Unmarshal(w.Body.Bytes(), &filters); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(filters) == 0 {
		t.Error("expected filters, got empty")
	}
}

// TestPortfolio_ListFilters_Empty verifies that an empty database returns an empty filters array.
func TestPortfolio_ListFilters_Empty(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(ListFilters)
	w := makeRequest(handler, "GET", "/api/portfolio/filters", nil)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var filters []FilterCategory
	if err := json.Unmarshal(w.Body.Bytes(), &filters); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(filters) != 0 {
		t.Errorf("expected 0 filters, got %d", len(filters))
	}
}

// =============================================================================
// Get Image Tests
// =============================================================================

// TestPortfolio_GetImage verifies that a single image can be retrieved by its timestamp ID.
func TestPortfolio_GetImage(t *testing.T) {
	resetTestData(t)

	// Use timestamp-based image URL (matches upload pattern: portfolio/{timestamp}.jpg)
	timestamp := "1234567890123456789" // 19 digits = valid nanosecond timestamp
	url := "https://example.com/portfolio/" + timestamp + ".jpg"
	thumbURL := "https://example.com/portfolio/" + timestamp + "_thumb.jpg"

	// Insert test image with timestamp-based URL
	var imageID string
	err := db.DB.QueryRow(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES ($1, $2, ARRAY['nature:forest'])
		RETURNING id
	`, url, thumbURL).Scan(&imageID)
	if err != nil {
		t.Fatalf("failed to create image: %v", err)
	}

	// Create request with chi URLParam context
	req := httptest.NewRequest("GET", "/api/portfolio/images/"+timestamp, nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", timestamp)
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

	w := httptest.NewRecorder()
	GetImage(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var img Image
	if err := json.Unmarshal(w.Body.Bytes(), &img); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if img.ID != imageID {
		t.Errorf("expected image ID %s, got %s", imageID, img.ID)
	}
}

// TestPortfolio_GetImage_NotFound verifies that requesting a non-existent image returns 404.
func TestPortfolio_GetImage_NotFound(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(GetImage)
	w := makeRequest(handler, "GET", "/api/portfolio/images/nonexistent-id", nil)

	if w.Code != http.StatusNotFound {
		t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Upload Image Tests (Admin Only)
// =============================================================================

// TestPortfolio_Upload_Admin verifies that an admin user passes the authentication check for image upload.
func TestPortfolio_Upload_Admin(t *testing.T) {
	resetTestData(t)

	// Create a minimal S3 client mock by setting it to nil (handler will check and return error)
	// The handler requires S3 client, so we test the auth check first
	// Since S3 client setup is complex, we test that admin gets past auth check

	handler := http.HandlerFunc(UploadImage)
	w := makeRequestWithContext(handler, "POST", "/api/portfolio/images", nil, "admin-001", "admin")

	// Should not get 403 (forbidden), will get another error due to missing S3 or file
	// The important thing is it's not 403 for admin
	if w.Code == http.StatusForbidden {
		t.Error("admin should not get 403 - admin access should be granted")
	}
}

// TestPortfolio_Upload_NonAdmin verifies that non-admin users receive 403 Forbidden on image upload attempts.
func TestPortfolio_Upload_NonAdmin(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(UploadImage)
	w := makeRequestWithContext(handler, "POST", "/api/portfolio/images", nil, "user-001", "verified_email")

	if w.Code != http.StatusForbidden {
		t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestPortfolio_Upload_Unauthenticated verifies that unauthenticated requests receive 401 Unauthorized on image upload.
func TestPortfolio_Upload_Unauthenticated(t *testing.T) {
	resetTestData(t)

	handler := http.HandlerFunc(UploadImage)
	w := makeRequest(handler, "POST", "/api/portfolio/images", nil)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Delete Image Tests (Admin Only)
// =============================================================================

// TestPortfolio_Delete_Admin verifies that an admin user passes the authentication check for image deletion.
func TestPortfolio_Delete_Admin(t *testing.T) {
	resetTestData(t)

	// Insert test image
	var imageID string
	err := db.DB.QueryRow(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
		RETURNING id
	`).Scan(&imageID)
	if err != nil {
		t.Fatalf("failed to create image: %v", err)
	}

	handler := http.HandlerFunc(DeleteImage)
	w := makeRequestWithContext(handler, "DELETE", "/api/portfolio/images/"+imageID, nil, "admin-001", "admin")

	// Should not get 403 (forbidden) - will get error due to S3 client being nil
	// but the important thing is admin auth passed
	if w.Code == http.StatusForbidden {
		t.Error("admin should not get 403 - admin access should be granted")
	}
}

// TestPortfolio_Delete_NonAdmin verifies that non-admin users receive 403 Forbidden on image deletion attempts.
func TestPortfolio_Delete_NonAdmin(t *testing.T) {
	resetTestData(t)

	// Insert test image
	var imageID string
	err := db.DB.QueryRow(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
		RETURNING id
	`).Scan(&imageID)
	if err != nil {
		t.Fatalf("failed to create image: %v", err)
	}

	handler := http.HandlerFunc(DeleteImage)
	w := makeRequestWithContext(handler, "DELETE", "/api/portfolio/images/"+imageID, nil, "user-001", "verified_email")

	if w.Code != http.StatusForbidden {
		t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestPortfolio_Delete_Unauthenticated verifies that unauthenticated requests receive 401 Unauthorized on image deletion.
func TestPortfolio_Delete_Unauthenticated(t *testing.T) {
	resetTestData(t)

	// Insert test image
	var imageID string
	err := db.DB.QueryRow(context.Background(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
		RETURNING id
	`).Scan(&imageID)
	if err != nil {
		t.Fatalf("failed to create image: %v", err)
	}

	handler := http.HandlerFunc(DeleteImage)
	w := makeRequest(handler, "DELETE", "/api/portfolio/images/"+imageID, nil)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// processImage EXIF Stripping Tests
// =============================================================================

// TestPortfolio_ProcessImage_EXIFStripped verifies that processImage strips EXIF GPS data from images.
func TestPortfolio_ProcessImage_EXIFStripped(t *testing.T) {
	// Create a simple test image using the standard library
	img := image.NewNRGBA(image.Rect(0, 0, 100, 100))
	grayColor := color.RGBA{R: 200, G: 200, B: 200, A: 255}
	for y := 0; y < 100; y++ {
		for x := 0; x < 100; x++ {
			img.Set(x, y, grayColor)
		}
	}

	// Encode to JPEG bytes using imaging
	var buf bytes.Buffer
	err := imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(85))
	if err != nil {
		t.Fatalf("failed to encode test image: %v", err)
	}
	imageBytes := buf.Bytes()

	// Create a JPEG with embedded EXIF GPS data by appending GPS IFD marker after JPEG header
	// This creates a JPEG that claims to have GPS metadata
	gpsJpeg := createJpegWithExifMarker(imageBytes)
	if gpsJpeg == nil {
		t.Skip("Could not create JPEG with EXIF marker - using basic test")
	}

	// Verify EXIF marker is present in the input
	hasExifBefore := bytes.Contains(gpsJpeg, []byte{0xFF, 0xE1}) // APP1 EXIF marker
	if !hasExifBefore {
		t.Skip("Could not inject EXIF marker - skipping GPS stripping test")
	}

	// Process the image (this should strip EXIF/GPS data)
	result, err := processImage(gpsJpeg, 85)
	if err != nil {
		t.Fatalf("processImage failed: %v", err)
	}

	// Verify the result is a valid JPEG
	if len(result) == 0 {
		t.Fatal("processImage returned empty result")
	}

	// Verify EXIF marker is NOT present in the output
	hasExifAfter := bytes.Contains(result, []byte{0xFF, 0xE1})
	if hasExifAfter {
		t.Error("EXIF data was NOT stripped by processImage - metadata still present")
	}
}

// createJpegWithExifMarker creates a JPEG with an APP1 EXIF marker inserted after the SOI marker
func createJpegWithExifMarker(jpegData []byte) []byte {
	if len(jpegData) < 2 || jpegData[0] != 0xFF || jpegData[1] != 0xD8 {
		return nil
	}

	// Create a minimal APP1 EXIF marker with GPS IFD tag (0x8825)
	// APP1 marker: FF E1, length, "Exif\0\0", byte order, magic, IFD offset
	app1 := []byte{
		0xFF, 0xE1, // APP1 marker
		0x00, 0x22, // Length: 34 bytes
		// Exif header
		0x45, 0x78, 0x69, 0x66, 0x00, 0x00, // "Exif\0\0"
		0x49, 0x49, // Byte order: little-endian
		0x2A, 0x00, // Magic number
		0x08, 0x00, 0x00, 0x00, // Offset to first IFD
		// Main IFD with GPS IFD pointer
		0x01, 0x00, // Number of entries: 1
		0x25, 0x88, // GPS IFD tag (0x8825)
		0x04, 0x00, // Type: LONG
		0x01, 0x00, 0x00, 0x00, // Count: 1
		0x10, 0x00, 0x00, 0x00, // Offset: 16 (to GPS IFD)
		0x00, 0x00, 0x00, 0x00, // Next IFD: none
	}

	// Insert APP1 after SOI marker (FF D8)
	result := make([]byte, 0, len(jpegData)+len(app1))
	result = append(result, jpegData[:2]...)
	result = append(result, app1...)
	result = append(result, jpegData[2:]...)

	return result
}
