Support automatic response pipe addition in Kalamar endpoint

Change-Id: Ic76b53edaaa8c3ad7cee311900e301c723688796
diff --git a/cmd/termmapper/main.go b/cmd/termmapper/main.go
index 37de2da..90f6242 100644
--- a/cmd/termmapper/main.go
+++ b/cmd/termmapper/main.go
@@ -49,6 +49,14 @@
 	Mappings    []TemplateMapping
 }
 
+type QueryParams struct {
+	Dir      string
+	FoundryA string
+	FoundryB string
+	LayerA   string
+	LayerB   string
+}
+
 func parseConfig() *appConfig {
 	cfg := &appConfig{}
 
@@ -336,6 +344,27 @@
 	return func(c *fiber.Ctx) error {
 		mapID := c.Params("map")
 
+		// Get query parameters
+		dir := c.Query("dir", "atob")
+		foundryA := c.Query("foundryA", "")
+		foundryB := c.Query("foundryB", "")
+		layerA := c.Query("layerA", "")
+		layerB := c.Query("layerB", "")
+
+		// Validate input parameters (reuse existing validation)
+		if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, []byte{}); err != nil {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"error": err.Error(),
+			})
+		}
+
+		// Validate direction
+		if dir != "atob" && dir != "btoa" {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"error": "invalid direction, must be 'atob' or 'btoa'",
+			})
+		}
+
 		// Get list of available mappings
 		var mappings []TemplateMapping
 		for _, list := range yamlConfig.Lists {
@@ -363,8 +392,17 @@
 			Mappings:    mappings,
 		}
 
+		// Add query parameters to template data
+		queryParams := QueryParams{
+			Dir:      dir,
+			FoundryA: foundryA,
+			FoundryB: foundryB,
+			LayerA:   layerA,
+			LayerB:   layerB,
+		}
+
 		// Generate HTML
-		html := generateKalamarPluginHTML(data)
+		html := generateKalamarPluginHTML(data, queryParams)
 
 		c.Set("Content-Type", "text/html")
 		return c.SendString(html)
@@ -373,7 +411,7 @@
 
 // generateKalamarPluginHTML creates the HTML template for the Kalamar plugin page
 // This function can be easily modified to change the appearance and content
-func generateKalamarPluginHTML(data TemplateData) string {
+func generateKalamarPluginHTML(data TemplateData, queryParams QueryParams) string {
 	html := `<!DOCTYPE html>
 <html lang="en">
 <head>
@@ -419,7 +457,7 @@
 	}
 
 	html += `
-    </dl>`
+    </dl></div>`
 
 	if data.MapID != "" {
 
@@ -430,7 +468,10 @@
 
 		// Use path.Join to normalize the path part
 		queryServiceURL.Path = path.Join(queryServiceURL.Path, data.MapID+"/query")
-		queryServiceURL.RawQuery = "dir=atob"
+
+		// Build query parameters for query URL
+		queryParamString := buildQueryParams(queryParams.Dir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
+		queryServiceURL.RawQuery = queryParamString
 
 		responseServiceURL, err := url.Parse(data.ServiceURL)
 		if err != nil {
@@ -440,7 +481,16 @@
 		// Use path.Join to normalize the path part
 		responseServiceURL.Path = path.Join(responseServiceURL.Path, data.MapID+"/response")
 
-		html += `   <script>
+		reversedDir := "btoa"
+		if queryParams.Dir == "btoa" {
+			reversedDir = "atob"
+		}
+
+		// Build query parameters for response URL (with reversed direction)
+		responseParamString := buildQueryParams(reversedDir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
+		responseServiceURL.RawQuery = responseParamString
+
+		html += `<script>
   		<!-- activates/deactivates Mapper. -->
   		  
        let qdata = {
@@ -483,6 +533,27 @@
 	return html
 }
 
+// buildQueryParams builds a query string from the provided parameters
+func buildQueryParams(dir, foundryA, foundryB, layerA, layerB string) string {
+	params := url.Values{}
+	if dir != "" {
+		params.Add("dir", dir)
+	}
+	if foundryA != "" {
+		params.Add("foundryA", foundryA)
+	}
+	if foundryB != "" {
+		params.Add("foundryB", foundryB)
+	}
+	if layerA != "" {
+		params.Add("layerA", layerA)
+	}
+	if layerB != "" {
+		params.Add("layerB", layerB)
+	}
+	return params.Encode()
+}
+
 // expandGlobs expands glob patterns in the slice of file paths
 // Returns the expanded list of files or an error if glob expansion fails
 func expandGlobs(patterns []string) ([]string, error) {
diff --git a/cmd/termmapper/main_test.go b/cmd/termmapper/main_test.go
index 530ab68..80c7f4b 100644
--- a/cmd/termmapper/main_test.go
+++ b/cmd/termmapper/main_test.go
@@ -9,6 +9,7 @@
 	"os"
 	"path/filepath"
 	"sort"
+	"strings"
 	"testing"
 
 	tmconfig "github.com/KorAP/KoralPipe-TermMapper/config"
@@ -1345,8 +1346,205 @@
 				Mappings:    []TemplateMapping{},
 			}
 
-			html := generateKalamarPluginHTML(data)
+			// Use default query parameters for this test
+			queryParams := QueryParams{
+				Dir:      "atob",
+				FoundryA: "",
+				FoundryB: "",
+				LayerA:   "",
+				LayerB:   "",
+			}
+
+			html := generateKalamarPluginHTML(data, queryParams)
 			assert.Contains(t, html, tt.expected)
 		})
 	}
 }
+
+func TestKalamarPluginWithQueryParameters(t *testing.T) {
+	// Create test mapping list
+	mappingList := tmconfig.MappingList{
+		ID: "test-mapper",
+		Mappings: []tmconfig.MappingRule{
+			"[A] <> [B]",
+		},
+	}
+
+	// Create mapper
+	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
+	require.NoError(t, err)
+
+	// Create mock config
+	mockConfig := &tmconfig.MappingConfig{
+		ServiceURL: "https://example.com/plugin/termmapper",
+		Lists:      []tmconfig.MappingList{mappingList},
+	}
+
+	// Apply defaults
+	tmconfig.ApplyDefaults(mockConfig)
+
+	// Create fiber app
+	app := fiber.New()
+	setupRoutes(app, m, mockConfig)
+
+	tests := []struct {
+		name             string
+		url              string
+		expectedQueryURL string
+		expectedRespURL  string
+		expectedStatus   int
+		expectedError    string
+	}{
+		{
+			name:             "Default parameters (no query params)",
+			url:              "/test-mapper",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:             "Explicit dir=atob",
+			url:              "/test-mapper?dir=atob",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:             "Explicit dir=btoa",
+			url:              "/test-mapper?dir=btoa",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=btoa",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=atob",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:             "With foundry parameters",
+			url:              "/test-mapper?dir=atob&foundryA=opennlp&foundryB=upos",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa&foundryA=opennlp&foundryB=upos",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:             "With layer parameters",
+			url:              "/test-mapper?dir=btoa&layerA=pos&layerB=upos",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=btoa&layerA=pos&layerB=upos",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=atob&layerA=pos&layerB=upos",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:             "All parameters",
+			url:              "/test-mapper?dir=atob&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
+			expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
+			expectedRespURL:  "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
+			expectedStatus:   http.StatusOK,
+		},
+		{
+			name:           "Invalid direction",
+			url:            "/test-mapper?dir=invalid",
+			expectedStatus: http.StatusBadRequest,
+			expectedError:  "invalid direction, must be 'atob' or 'btoa'",
+		},
+		{
+			name:           "Parameter too long",
+			url:            "/test-mapper?foundryA=" + strings.Repeat("a", 1025),
+			expectedStatus: http.StatusBadRequest,
+			expectedError:  "foundryA too long (max 1024 bytes)",
+		},
+		{
+			name:           "Invalid characters in parameter",
+			url:            "/test-mapper?foundryA=invalid<>chars",
+			expectedStatus: http.StatusBadRequest,
+			expectedError:  "foundryA contains invalid characters",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := httptest.NewRequest(http.MethodGet, tt.url, nil)
+			resp, err := app.Test(req)
+			require.NoError(t, err)
+			defer resp.Body.Close()
+
+			assert.Equal(t, tt.expectedStatus, resp.StatusCode)
+
+			body, err := io.ReadAll(resp.Body)
+			require.NoError(t, err)
+
+			if tt.expectedError != "" {
+				// Check error message
+				var errResp fiber.Map
+				err = json.Unmarshal(body, &errResp)
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedError, errResp["error"])
+			} else {
+				htmlContent := string(body)
+
+				// Check that both query and response URLs are present with correct parameters
+				assert.Contains(t, htmlContent, "'service' : '"+tt.expectedQueryURL+"'")
+				assert.Contains(t, htmlContent, "'service' : '"+tt.expectedRespURL+"'")
+
+				// Ensure it's still a valid HTML page
+				assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
+				assert.Contains(t, htmlContent, "<!DOCTYPE html>")
+			}
+		})
+	}
+}
+
+func TestBuildQueryParams(t *testing.T) {
+	tests := []struct {
+		name     string
+		dir      string
+		foundryA string
+		foundryB string
+		layerA   string
+		layerB   string
+		expected string
+	}{
+		{
+			name:     "Only direction parameter",
+			dir:      "atob",
+			expected: "dir=atob",
+		},
+		{
+			name:     "All parameters",
+			dir:      "btoa",
+			foundryA: "opennlp",
+			foundryB: "upos",
+			layerA:   "pos",
+			layerB:   "upos",
+			expected: "dir=btoa&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
+		},
+		{
+			name:     "Some parameters empty",
+			dir:      "atob",
+			foundryA: "opennlp",
+			foundryB: "",
+			layerA:   "pos",
+			layerB:   "",
+			expected: "dir=atob&foundryA=opennlp&layerA=pos",
+		},
+		{
+			name:     "All parameters empty",
+			dir:      "",
+			foundryA: "",
+			foundryB: "",
+			layerA:   "",
+			layerB:   "",
+			expected: "",
+		},
+		{
+			name:     "URL encoding needed",
+			dir:      "atob",
+			foundryA: "test space",
+			foundryB: "test&special",
+			expected: "dir=atob&foundryA=test+space&foundryB=test%26special",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := buildQueryParams(tt.dir, tt.foundryA, tt.foundryB, tt.layerA, tt.layerB)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}