package middleware

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
)

func TestInputGate_AllowsGETRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for GET request, got %d", w.Code)
	}
}

func TestInputGate_AllowsHEADRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.HEAD("/test", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodHead, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for HEAD request, got %d", w.Code)
	}
}

func TestInputGate_AllowsOPTIONSRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.OPTIONS("/test", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodOptions, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for OPTIONS request, got %d", w.Code)
	}
}

func TestInputGate_AllowsValidJSONRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	body := bytes.NewBufferString(`{"key": "value"}`)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Content-Length", "16")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for valid JSON, got %d", w.Code)
	}
}

func TestInputGate_RejectsInvalidContentType(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultInputGateConfig()
	config.StrictContentType = true
	router.Use(InputGateWithConfig(config))

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	body := bytes.NewBufferString(`data`)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	req.Header.Set("Content-Type", "application/xml")
	req.Header.Set("Content-Length", "4")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusUnsupportedMediaType {
		t.Errorf("Expected status 415 for invalid content type, got %d", w.Code)
	}
}

func TestInputGate_AllowsEmptyContentType(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	body := bytes.NewBufferString(`data`)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	// No Content-Type header
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for empty content type, got %d", w.Code)
	}
}

func TestInputGate_RejectsOversizedRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultInputGateConfig()
	config.MaxBodySize = 100 // 100 bytes
	router.Use(InputGateWithConfig(config))

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	// Create a body larger than 100 bytes
	largeBody := strings.Repeat("x", 200)
	body := bytes.NewBufferString(largeBody)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Content-Length", "200")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusRequestEntityTooLarge {
		t.Errorf("Expected status 413 for oversized request, got %d", w.Code)
	}
}

func TestInputGate_AllowsLargeUploadPath(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultInputGateConfig()
	config.MaxBodySize = 100              // 100 bytes
	config.MaxFileSize = 1000             // 1000 bytes
	config.LargeUploadPaths = []string{"/api/v1/files/upload"}
	router.Use(InputGateWithConfig(config))

	router.POST("/api/v1/files/upload", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	// Create a body larger than MaxBodySize but smaller than MaxFileSize
	largeBody := strings.Repeat("x", 500)
	body := bytes.NewBufferString(largeBody)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/files/upload", body)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Content-Length", "500")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for large upload path, got %d", w.Code)
	}
}

func TestInputGate_ExcludedPaths(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultInputGateConfig()
	config.MaxBodySize = 10 // Very small
	config.ExcludedPaths = []string{"/health"}
	router.Use(InputGateWithConfig(config))

	router.POST("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "healthy"})
	})

	// Send oversized body to excluded path
	largeBody := strings.Repeat("x", 100)
	body := bytes.NewBufferString(largeBody)
	req := httptest.NewRequest(http.MethodPost, "/health", body)
	req.Header.Set("Content-Length", "100")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Should pass because path is excluded
	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 for excluded path, got %d", w.Code)
	}
}

func TestInputGate_RejectsInvalidContentLength(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	body := bytes.NewBufferString(`data`)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Content-Length", "invalid")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status 400 for invalid content length, got %d", w.Code)
	}
}

func TestValidateFileUpload_BlockedExtension(t *testing.T) {
	tests := []struct {
		filename    string
		contentType string
		blocked     bool
	}{
		{"malware.exe", "application/octet-stream", true},
		{"script.bat", "application/octet-stream", true},
		{"hack.cmd", "application/octet-stream", true},
		{"shell.sh", "application/octet-stream", true},
		{"powershell.ps1", "application/octet-stream", true},
		{"document.pdf", "application/pdf", false},
		{"image.jpg", "image/jpeg", false},
		{"data.csv", "text/csv", false},
	}

	for _, tt := range tests {
		valid, errMsg := ValidateFileUpload(tt.filename, tt.contentType, 100, nil)
		if tt.blocked && valid {
			t.Errorf("File %s should be blocked", tt.filename)
		}
		if !tt.blocked && !valid {
			t.Errorf("File %s should not be blocked, error: %s", tt.filename, errMsg)
		}
	}
}

func TestValidateFileUpload_OversizedFile(t *testing.T) {
	config := DefaultInputGateConfig()
	config.MaxFileSize = 1000 // 1KB

	valid, errMsg := ValidateFileUpload("test.pdf", "application/pdf", 2000, &config)

	if valid {
		t.Error("Should reject oversized file")
	}
	if !strings.Contains(errMsg, "size") {
		t.Errorf("Error message should mention size, got: %s", errMsg)
	}
}

func TestValidateFileUpload_ValidFile(t *testing.T) {
	config := DefaultInputGateConfig()

	valid, errMsg := ValidateFileUpload("document.pdf", "application/pdf", 1000, &config)

	if !valid {
		t.Errorf("Should accept valid file, got error: %s", errMsg)
	}
}

func TestValidateFileUpload_InvalidContentType(t *testing.T) {
	config := DefaultInputGateConfig()

	valid, errMsg := ValidateFileUpload("file.xyz", "application/x-unknown", 100, &config)

	if valid {
		t.Error("Should reject unknown file type")
	}
	if !strings.Contains(errMsg, "not allowed") {
		t.Errorf("Error message should mention not allowed, got: %s", errMsg)
	}
}

func TestValidateFileUpload_NilConfig(t *testing.T) {
	// Should use default config when nil is passed
	valid, _ := ValidateFileUpload("document.pdf", "application/pdf", 1000, nil)

	if !valid {
		t.Error("Should accept valid file with nil config (uses defaults)")
	}
}

func TestHasBlockedExtension(t *testing.T) {
	config := DefaultInputGateConfig()

	tests := []struct {
		filename string
		blocked  bool
	}{
		{"test.exe", true},
		{"TEST.EXE", true}, // Case insensitive
		{"script.BAT", true},
		{"app.APP", true},
		{"document.pdf", false},
		{"image.png", false},
		{"", false},
	}

	for _, tt := range tests {
		result := config.hasBlockedExtension(tt.filename)
		if result != tt.blocked {
			t.Errorf("File %s: expected blocked=%v, got %v", tt.filename, tt.blocked, result)
		}
	}
}

func TestValidateContentType(t *testing.T) {
	config := DefaultInputGateConfig()

	tests := []struct {
		contentType string
		valid       bool
	}{
		{"application/json", true},
		{"application/json; charset=utf-8", true},
		{"APPLICATION/JSON", true}, // Case insensitive
		{"multipart/form-data; boundary=----WebKitFormBoundary", true},
		{"text/plain", true},
		{"application/xml", false},
		{"text/html", false},
		{"", true}, // Empty is allowed
	}

	for _, tt := range tests {
		valid, _ := config.validateContentType(tt.contentType)
		if valid != tt.valid {
			t.Errorf("Content-Type %q: expected valid=%v, got %v", tt.contentType, tt.valid, valid)
		}
	}
}

func TestIsLargeUploadPath(t *testing.T) {
	config := DefaultInputGateConfig()
	config.LargeUploadPaths = []string{"/api/v1/files/upload", "/api/v1/documents"}

	tests := []struct {
		path     string
		isLarge  bool
	}{
		{"/api/v1/files/upload", true},
		{"/api/v1/files/upload/batch", true}, // Prefix match
		{"/api/v1/documents", true},
		{"/api/v1/documents/1/attachments", true},
		{"/api/v1/users", false},
		{"/health", false},
	}

	for _, tt := range tests {
		result := config.isLargeUploadPath(tt.path)
		if result != tt.isLarge {
			t.Errorf("Path %s: expected isLarge=%v, got %v", tt.path, tt.isLarge, result)
		}
	}
}

func TestGetMaxSize(t *testing.T) {
	config := DefaultInputGateConfig()
	config.MaxBodySize = 100
	config.MaxFileSize = 1000
	config.LargeUploadPaths = []string{"/api/v1/files/upload"}

	tests := []struct {
		path     string
		expected int64
	}{
		{"/api/test", 100},
		{"/api/v1/files/upload", 1000},
		{"/health", 100},
	}

	for _, tt := range tests {
		result := config.getMaxSize(tt.path)
		if result != tt.expected {
			t.Errorf("Path %s: expected maxSize=%d, got %d", tt.path, tt.expected, result)
		}
	}
}

func TestInputGate_DefaultMiddleware(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(InputGate())

	router.POST("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	body := bytes.NewBufferString(`{"key": "value"}`)
	req := httptest.NewRequest(http.MethodPost, "/test", body)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d", w.Code)
	}
}
