package main

import (
	"bytes"
	"encoding/json"
	"html/template"
	"io"
	"io/fs"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	tmconfig "github.com/KorAP/Koral-Mapper/config"
	"github.com/KorAP/Koral-Mapper/mapper"
	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// jsEscapeURL converts a URL to its html/template JS string escaped form.
// In JS string context, html/template escapes / as \/ and & as \u0026.
func jsEscapeURL(u string) string {
	u = strings.ReplaceAll(u, "/", `\/`)
	u = strings.ReplaceAll(u, "&", `\u0026`)
	return u
}

func loadConfigFromYAML(t *testing.T, configYAML string, mappingYAMLs ...string) *tmconfig.MappingConfig {
	t.Helper()

	configPath := ""
	if configYAML != "" {
		cfgFile, err := os.CreateTemp("", "koralmapper-config-*.yaml")
		require.NoError(t, err)
		t.Cleanup(func() { _ = os.Remove(cfgFile.Name()) })
		_, err = cfgFile.WriteString(configYAML)
		require.NoError(t, err)
		require.NoError(t, cfgFile.Close())
		configPath = cfgFile.Name()
	}

	mappingPaths := make([]string, 0, len(mappingYAMLs))
	for _, content := range mappingYAMLs {
		mapFile, err := os.CreateTemp("", "koralmapper-mapping-*.yaml")
		require.NoError(t, err)
		t.Cleanup(func() { _ = os.Remove(mapFile.Name()) })
		_, err = mapFile.WriteString(content)
		require.NoError(t, err)
		require.NoError(t, mapFile.Close())
		mappingPaths = append(mappingPaths, mapFile.Name())
	}

	cfg, err := tmconfig.LoadFromSources(configPath, mappingPaths)
	require.NoError(t, err)
	return cfg
}

func TestTransformEndpoint(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: true
    mappings:
      - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
      - "[DET] <> [opennlp/p=DET]"
`)

	// Create mapper
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	// Create fiber app
	app := fiber.New()
	setupRoutes(app, m, cfg)

	tests := []struct {
		name          string
		mapID         string
		direction     string
		foundryA      string
		foundryB      string
		layerA        string
		layerB        string
		input         string
		expectedCode  int
		expectedBody  string
		expectedError string
	}{
		{
			name:      "Simple A to B mapping",
			mapID:     "test-mapper",
			direction: "atob",
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "opennlp",
					"key": "PIDAT",
					"layer": "p",
					"match": "match:eq"
				}
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:termGroup",
					"operands": [
						{
							"@type": "koral:term",
							"foundry": "opennlp",
							"key": "PIDAT",
							"layer": "p",
							"match": "match:eq"
						},
						{
							"@type": "koral:term",
							"foundry": "opennlp",
							"key": "AdjType",
							"layer": "p",
							"match": "match:eq",
							"value": "Pdt"
						}
					],
					"relation": "relation:and",
					"rewrites": [
						{
							"@type": "koral:rewrite",
							"editor": "Koral-Mapper",
							"original": {
								"@type": "koral:term",
								"foundry": "opennlp",
								"key": "PIDAT",
								"layer": "p",
								"match": "match:eq"
							}
						}
					]
				}
			}`,
		},
		{
			name:      "B to A mapping",
			mapID:     "test-mapper",
			direction: "btoa",
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:termGroup",
					"operands": [
						{
							"@type": "koral:term",
							"foundry": "opennlp",
							"key": "PIDAT",
							"layer": "p",
							"match": "match:eq"
						},
						{
							"@type": "koral:term",
							"foundry": "opennlp",
							"key": "AdjType",
							"layer": "p",
							"match": "match:eq",
							"value": "Pdt"
						}
					],
					"relation": "relation:and"
				}
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "opennlp",
					"key": "PIDAT",
					"layer": "p",
					"match": "match:eq",
					"rewrites": [
						{
							"@type": "koral:rewrite",
							"editor": "Koral-Mapper",
							"original": {
								"@type": "koral:termGroup",
								"operands": [
									{
										"@type": "koral:term",
										"foundry": "opennlp",
										"key": "PIDAT",
										"layer": "p",
										"match": "match:eq"
									},
									{
										"@type": "koral:term",
										"foundry": "opennlp",
										"key": "AdjType",
										"layer": "p",
										"match": "match:eq",
										"value": "Pdt"
									}
								],
								"relation": "relation:and"
							}
						}
					]
				}
			}`,
		},
		{
			name:      "Mapping with foundry override",
			mapID:     "test-mapper",
			direction: "atob",
			foundryB:  "custom",
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "opennlp",
					"key": "PIDAT",
					"layer": "p",
					"match": "match:eq"
				}
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:termGroup",
					"operands": [
						{
							"@type": "koral:term",
							"foundry": "custom",
							"key": "PIDAT",
							"layer": "p",
							"match": "match:eq"
						},
						{
							"@type": "koral:term",
							"foundry": "custom",
							"key": "AdjType",
							"layer": "p",
							"match": "match:eq",
							"value": "Pdt"
						}
					],
					"relation": "relation:and",
					"rewrites": [
						{
							"@type": "koral:rewrite",
							"editor": "Koral-Mapper",
							"original": {
								"@type": "koral:term",
								"foundry": "opennlp",
								"key": "PIDAT",
								"layer": "p",
								"match": "match:eq"
							}
						}
					]
				}
			}`,
		},
		{
			name:          "Invalid mapping ID",
			mapID:         "nonexistent",
			direction:     "atob",
			input:         `{"@type": "koral:token"}`,
			expectedCode:  http.StatusInternalServerError,
			expectedError: "mapping list with ID nonexistent not found",
		},
		{
			name:          "Invalid direction",
			mapID:         "test-mapper",
			direction:     "invalid",
			input:         `{"@type": "koral:token"}`,
			expectedCode:  http.StatusBadRequest,
			expectedError: "invalid direction, must be 'atob' or 'btoa'",
		},
		{
			name:          "Invalid JSON",
			mapID:         "test-mapper",
			direction:     "atob",
			input:         `invalid json`,
			expectedCode:  http.StatusBadRequest,
			expectedError: "invalid JSON in request body",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Build URL with query parameters
			url := "/" + tt.mapID + "/query"
			if tt.direction != "" {
				url += "?dir=" + tt.direction
			}
			if tt.foundryA != "" {
				url += "&foundryA=" + tt.foundryA
			}
			if tt.foundryB != "" {
				url += "&foundryB=" + tt.foundryB
			}
			if tt.layerA != "" {
				url += "&layerA=" + tt.layerA
			}
			if tt.layerB != "" {
				url += "&layerB=" + tt.layerB
			}

			// Make request
			req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
			req.Header.Set("Content-Type", "application/json")
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			// Check status code
			assert.Equal(t, tt.expectedCode, resp.StatusCode)

			// Read response body
			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 {
				// Compare JSON responses
				var expected, actual any
				err = json.Unmarshal([]byte(tt.expectedBody), &expected)
				require.NoError(t, err)
				err = json.Unmarshal(body, &actual)
				require.NoError(t, err)
				assert.Equal(t, expected, actual)
			}
		})
	}
}

func TestResponseTransformEndpoint(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-response-mapper
    foundryA: marmot
    layerA: m
    foundryB: opennlp
    layerB: p
    mappings:
      - "[gender:masc] <> [p=M & m=M]"
`)

	// Create mapper
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	// Create fiber app
	app := fiber.New()
	setupRoutes(app, m, cfg)

	tests := []struct {
		name          string
		mapID         string
		direction     string
		foundryA      string
		foundryB      string
		layerA        string
		layerB        string
		input         string
		expectedCode  int
		expectedBody  string
		expectedError string
	}{
		{
			name:      "Simple response mapping with snippet transformation",
			mapID:     "test-response-mapper",
			direction: "atob",
			input: `{
				"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"snippet": "<span title=\"marmot/m:gender:masc\"><span title=\"opennlp/p:M\" class=\"notinindex\"><span title=\"opennlp/m:M\" class=\"notinindex\">Der</span></span></span>"
			}`,
		},
		{
			name:      "Response with no snippet field",
			mapID:     "test-response-mapper",
			direction: "atob",
			input: `{
				"@type": "koral:response",
				"meta": {
					"version": "Krill-0.64.1"
				}
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"@type": "koral:response",
				"meta": {
					"version": "Krill-0.64.1"
				}
			}`,
		},
		{
			name:      "Response with null snippet",
			mapID:     "test-response-mapper",
			direction: "atob",
			input: `{
				"snippet": null
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"snippet": null
			}`,
		},
		{
			name:      "Response with non-string snippet",
			mapID:     "test-response-mapper",
			direction: "atob",
			input: `{
				"snippet": 123
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"snippet": 123
			}`,
		},
		{
			name:      "Response mapping with foundry override",
			mapID:     "test-response-mapper",
			direction: "atob",
			foundryB:  "custom",
			input: `{
				"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
			}`,
			expectedCode: http.StatusOK,
			expectedBody: `{
				"snippet": "<span title=\"marmot/m:gender:masc\"><span title=\"custom/p:M\" class=\"notinindex\"><span title=\"custom/m:M\" class=\"notinindex\">Der</span></span></span>"
			}`,
		},
		{
			name:          "Invalid mapping ID for response",
			mapID:         "nonexistent",
			direction:     "atob",
			input:         `{"snippet": "<span>test</span>"}`,
			expectedCode:  http.StatusInternalServerError,
			expectedError: "mapping list with ID nonexistent not found",
		},
		{
			name:          "Invalid direction for response",
			mapID:         "test-response-mapper",
			direction:     "invalid",
			input:         `{"snippet": "<span>test</span>"}`,
			expectedCode:  http.StatusBadRequest,
			expectedError: "invalid direction, must be 'atob' or 'btoa'",
		},
		{
			name:          "Invalid JSON for response",
			mapID:         "test-response-mapper",
			direction:     "atob",
			input:         `{invalid json}`,
			expectedCode:  http.StatusBadRequest,
			expectedError: "invalid JSON in request body",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Build URL with query parameters
			url := "/" + tt.mapID + "/response"
			if tt.direction != "" {
				url += "?dir=" + tt.direction
			}
			if tt.foundryA != "" {
				url += "&foundryA=" + tt.foundryA
			}
			if tt.foundryB != "" {
				url += "&foundryB=" + tt.foundryB
			}
			if tt.layerA != "" {
				url += "&layerA=" + tt.layerA
			}
			if tt.layerB != "" {
				url += "&layerB=" + tt.layerB
			}

			// Make request
			req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
			req.Header.Set("Content-Type", "application/json")
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			// Check status code
			assert.Equal(t, tt.expectedCode, resp.StatusCode)

			// Read response body
			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 {
				// Compare JSON responses
				var expected, actual any
				err = json.Unmarshal([]byte(tt.expectedBody), &expected)
				require.NoError(t, err)
				err = json.Unmarshal(body, &actual)
				require.NoError(t, err)
				assert.Equal(t, expected, actual)
			}
		})
	}
}

func TestHealthEndpoint(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 for testing
	mockConfig := &tmconfig.MappingConfig{
		Lists: []tmconfig.MappingList{mappingList},
	}

	// Create fiber app
	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// Test health endpoint
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	assert.Equal(t, "OK", string(body))

	req = httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err = app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	assert.Contains(t, string(body), "Koral-Mapper")

}

func TestKalamarPluginWithCustomSdkAndServer(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)

	tests := []struct {
		name           string
		customSDK      string
		customServer   string
		expectedSDK    string
		expectedServer string
	}{
		{
			name:           "Custom SDK and Server values",
			customSDK:      "https://custom.example.com/custom-sdk.js",
			customServer:   "https://custom.example.com/",
			expectedSDK:    "https://custom.example.com/custom-sdk.js",
			expectedServer: "https://custom.example.com/",
		},
		{
			name:           "Only custom SDK value",
			customSDK:      "https://custom.example.com/custom-sdk.js",
			customServer:   "https://korap.ids-mannheim.de/", // defaults applied during parsing
			expectedSDK:    "https://custom.example.com/custom-sdk.js",
			expectedServer: "https://korap.ids-mannheim.de/",
		},
		{
			name:           "Only custom Server value",
			customSDK:      "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
			customServer:   "https://custom.example.com/",
			expectedSDK:    "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
			expectedServer: "https://custom.example.com/",
		},
		{
			name:           "Defaults applied during parsing",
			customSDK:      "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
			customServer:   "https://korap.ids-mannheim.de/",                          // defaults applied during parsing
			expectedSDK:    "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
			expectedServer: "https://korap.ids-mannheim.de/",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock config with custom values
			mockConfig := &tmconfig.MappingConfig{
				SDK:    tt.customSDK,
				Server: tt.customServer,
				Lists:  []tmconfig.MappingList{mappingList},
			}

			// Create fiber app
			app := fiber.New()
			setupRoutes(app, m, mockConfig)

			// Test Kalamar plugin endpoint
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, http.StatusOK, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			htmlContent := string(body)

			// Check that the HTML contains the expected SDK and Server values
			assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
			assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)

			// Ensure it's still a valid HTML page
			assert.Contains(t, htmlContent, "Koral-Mapper")
			assert.Contains(t, htmlContent, "<!DOCTYPE html>")
		})
	}
}

func TestMultipleMappingFiles(t *testing.T) {
	// Create test mapping files
	mappingFile1Content := `
id: test-mapper-1
foundryA: opennlp
layerA: p
foundryB: upos
layerB: p
mappings:
  - "[PIDAT] <> [DET & AdjType=Pdt]"
  - "[PAV] <> [ADV & PronType=Dem]"
`
	mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mappingFile1.Name())

	_, err = mappingFile1.WriteString(mappingFile1Content)
	require.NoError(t, err)
	err = mappingFile1.Close()
	require.NoError(t, err)

	mappingFile2Content := `
id: test-mapper-2
foundryA: stts
layerA: p
foundryB: upos
layerB: p
mappings:
  - "[DET] <> [PRON]"
  - "[ADJ] <> [NOUN]"
`
	mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mappingFile2.Name())

	_, err = mappingFile2.WriteString(mappingFile2Content)
	require.NoError(t, err)
	err = mappingFile2.Close()
	require.NoError(t, err)

	// Load configuration using multiple mapping files
	config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
	require.NoError(t, err)

	// Create mapper
	m, err := mapper.NewMapper(config.Lists)
	require.NoError(t, err)

	// Create fiber app
	app := fiber.New()
	setupRoutes(app, m, config)

	// Test that both mappers work
	testCases := []struct {
		name        string
		mapID       string
		input       string
		expectGroup bool
		expectedKey string
	}{
		{
			name:  "test-mapper-1 with complex mapping",
			mapID: "test-mapper-1",
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "opennlp",
					"key": "PIDAT",
					"layer": "p",
					"match": "match:eq"
				}
			}`,
			expectGroup: true,  // This mapping creates a termGroup because of "&"
			expectedKey: "DET", // The first operand should be DET
		},
		{
			name:  "test-mapper-2 with simple mapping",
			mapID: "test-mapper-2",
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "stts",
					"key": "DET",
					"layer": "p",
					"match": "match:eq"
				}
			}`,
			expectGroup: false, // This mapping creates a simple term
			expectedKey: "PRON",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
			req.Header.Set("Content-Type", "application/json")

			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, http.StatusOK, resp.StatusCode)

			var result map[string]any
			err = json.NewDecoder(resp.Body).Decode(&result)
			require.NoError(t, err)

			// Check that the mapping was applied
			wrap := result["wrap"].(map[string]any)
			if tc.expectGroup {
				// For complex mappings, check the first operand
				assert.Equal(t, "koral:termGroup", wrap["@type"])
				operands := wrap["operands"].([]any)
				require.Greater(t, len(operands), 0)
				firstOperand := operands[0].(map[string]any)
				assert.Equal(t, tc.expectedKey, firstOperand["key"])
			} else {
				// For simple mappings, check the key directly
				assert.Equal(t, "koral:term", wrap["@type"])
				assert.Equal(t, tc.expectedKey, wrap["key"])
			}
		})
	}
}

func TestCombinedConfigAndMappingFiles(t *testing.T) {
	// Create main config file
	mainConfigContent := `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
lists:
- id: main-mapper
  foundryA: opennlp
  layerA: p
  mappings:
    - "[A] <> [B]"
`
	mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mainConfigFile.Name())

	_, err = mainConfigFile.WriteString(mainConfigContent)
	require.NoError(t, err)
	err = mainConfigFile.Close()
	require.NoError(t, err)

	// Create individual mapping file
	mappingFileContent := `
id: additional-mapper
foundryA: stts
layerA: p
mappings:
  - "[C] <> [D]"
`
	mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mappingFile.Name())

	_, err = mappingFile.WriteString(mappingFileContent)
	require.NoError(t, err)
	err = mappingFile.Close()
	require.NoError(t, err)

	// Load configuration from both sources
	config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
	require.NoError(t, err)

	// Verify that both mappers are loaded
	require.Len(t, config.Lists, 2)

	ids := make([]string, len(config.Lists))
	for i, list := range config.Lists {
		ids[i] = list.ID
	}
	assert.Contains(t, ids, "main-mapper")
	assert.Contains(t, ids, "additional-mapper")

	// Verify custom SDK and server are preserved from main config
	assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
	assert.Equal(t, "https://custom.example.com/", config.Server)

	// Create mapper and test it works
	m, err := mapper.NewMapper(config.Lists)
	require.NoError(t, err)
	require.NotNil(t, m)
}

func TestExpandGlobs(t *testing.T) {
	// Create a temporary directory for test files
	tempDir, err := os.MkdirTemp("", "glob_test_*")
	require.NoError(t, err)
	defer os.RemoveAll(tempDir)

	// Create test files with .yaml and .yml extensions
	testFiles := []struct {
		name    string
		content string
	}{
		{
			name: "mapper1.yaml",
			content: `
id: test-mapper-1
mappings:
  - "[A] <> [B]"
`,
		},
		{
			name: "mapper2.yml",
			content: `
id: test-mapper-2
mappings:
  - "[C] <> [D]"
`,
		},
		{
			name: "mapper3.yaml",
			content: `
id: test-mapper-3
mappings:
  - "[E] <> [F]"
`,
		},
		{
			name:    "other.txt",
			content: "not a yaml file",
		},
	}

	for _, file := range testFiles {
		filePath := filepath.Join(tempDir, file.name)
		err := os.WriteFile(filePath, []byte(file.content), 0644)
		require.NoError(t, err)
	}

	tests := []struct {
		name      string
		patterns  []string
		expected  []string
		expectErr bool
	}{
		{
			name:     "Single literal file",
			patterns: []string{filepath.Join(tempDir, "mapper1.yaml")},
			expected: []string{filepath.Join(tempDir, "mapper1.yaml")},
		},
		{
			name:     "Multiple literal files",
			patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
			expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
		},
		{
			name:     "Glob pattern for yaml files",
			patterns: []string{filepath.Join(tempDir, "*.yaml")},
			expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper3.yaml")},
		},
		{
			name:     "Glob pattern for yml files",
			patterns: []string{filepath.Join(tempDir, "*.yml")},
			expected: []string{filepath.Join(tempDir, "mapper2.yml")},
		},
		{
			name:     "Glob pattern for all yaml/yml files",
			patterns: []string{filepath.Join(tempDir, "*.y*ml")},
			expected: []string{
				filepath.Join(tempDir, "mapper1.yaml"),
				filepath.Join(tempDir, "mapper2.yml"),
				filepath.Join(tempDir, "mapper3.yaml"),
			},
		},
		{
			name:     "Mixed literal and glob",
			patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "*.yml")},
			expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
		},
		{
			name:     "No matches - treats as literal",
			patterns: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
			expected: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
		},
		{
			name:      "Invalid glob pattern",
			patterns:  []string{filepath.Join(tempDir, "[")},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := expandGlobs(tt.patterns)

			if tt.expectErr {
				assert.Error(t, err)
				return
			}

			require.NoError(t, err)

			// Sort both slices for comparison since glob results may not be in consistent order
			sort.Strings(result)
			sort.Strings(tt.expected)

			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestGlobMappingFileLoading(t *testing.T) {
	// Create a temporary directory for test files
	tempDir, err := os.MkdirTemp("", "glob_mapping_test_*")
	require.NoError(t, err)
	defer os.RemoveAll(tempDir)

	// Create test mapping files
	testFiles := []struct {
		name    string
		content string
	}{
		{
			name: "pos-mapper.yaml",
			content: `
id: pos-mapper
foundryA: opennlp
layerA: p
foundryB: upos
layerB: p
mappings:
  - "[PIDAT] <> [DET]"
  - "[ADJA] <> [ADJ]"
`,
		},
		{
			name: "ner-mapper.yml",
			content: `
id: ner-mapper
foundryA: opennlp
layerA: ner
foundryB: upos
layerB: ner
mappings:
  - "[PER] <> [PERSON]"
  - "[LOC] <> [LOCATION]"
`,
		},
		{
			name: "special-mapper.yaml",
			content: `
id: special-mapper
mappings:
  - "[X] <> [Y]"
`,
		},
	}

	for _, file := range testFiles {
		filePath := filepath.Join(tempDir, file.name)
		err := os.WriteFile(filePath, []byte(file.content), 0644)
		require.NoError(t, err)
	}

	tests := []struct {
		name           string
		configFile     string
		mappingPattern string
		expectedIDs    []string
	}{
		{
			name:           "Load all yaml files",
			mappingPattern: filepath.Join(tempDir, "*.yaml"),
			expectedIDs:    []string{"pos-mapper", "special-mapper"},
		},
		{
			name:           "Load all yml files",
			mappingPattern: filepath.Join(tempDir, "*.yml"),
			expectedIDs:    []string{"ner-mapper"},
		},
		{
			name:           "Load all yaml/yml files",
			mappingPattern: filepath.Join(tempDir, "*-mapper.y*ml"),
			expectedIDs:    []string{"pos-mapper", "ner-mapper", "special-mapper"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Expand the glob pattern
			expanded, err := expandGlobs([]string{tt.mappingPattern})
			require.NoError(t, err)

			// Load configuration using the expanded file list
			config, err := tmconfig.LoadFromSources(tt.configFile, expanded)
			require.NoError(t, err)

			// Verify that the expected mappers are loaded
			require.Len(t, config.Lists, len(tt.expectedIDs))

			actualIDs := make([]string, len(config.Lists))
			for i, list := range config.Lists {
				actualIDs[i] = list.ID
			}

			// Sort both slices for comparison
			sort.Strings(actualIDs)
			sort.Strings(tt.expectedIDs)
			assert.Equal(t, tt.expectedIDs, actualIDs)

			// Create mapper to ensure all loaded configs are valid
			m, err := mapper.NewMapper(config.Lists)
			require.NoError(t, err)
			require.NotNil(t, m)
		})
	}
}

func TestGlobErrorHandling(t *testing.T) {
	tests := []struct {
		name      string
		patterns  []string
		expectErr bool
	}{
		{
			name:      "Empty patterns",
			patterns:  []string{},
			expectErr: false, // Should return empty slice, no error
		},
		{
			name:      "Invalid glob pattern",
			patterns:  []string{"["},
			expectErr: true,
		},
		{
			name:      "Valid and invalid mixed",
			patterns:  []string{"valid.yaml", "["},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := expandGlobs(tt.patterns)

			if tt.expectErr {
				assert.Error(t, err)
				assert.Nil(t, result)
			} else {
				assert.NoError(t, err)
				if len(tt.patterns) == 0 {
					assert.Empty(t, result)
				}
			}
		})
	}
}

func TestGlobIntegrationWithTestData(t *testing.T) {
	// Test that our glob functionality works with the actual testdata files
	// This ensures the feature works end-to-end in a realistic scenario

	// Expand glob pattern for the example mapper files
	expanded, err := expandGlobs([]string{"../../testdata/example-mapper*.yaml"})
	require.NoError(t, err)

	// Should match exactly the two mapper files
	sort.Strings(expanded)
	assert.Len(t, expanded, 2)
	assert.Contains(t, expanded[0], "example-mapper1.yaml")
	assert.Contains(t, expanded[1], "example-mapper2.yaml")

	// Load configuration using the expanded files
	config, err := tmconfig.LoadFromSources("", expanded)
	require.NoError(t, err)

	// Verify that both mappers are loaded correctly
	require.Len(t, config.Lists, 2)

	// Get the IDs to verify they match the expected ones
	actualIDs := make([]string, len(config.Lists))
	for i, list := range config.Lists {
		actualIDs[i] = list.ID
	}
	sort.Strings(actualIDs)

	expectedIDs := []string{"example-mapper-1", "example-mapper-2"}
	assert.Equal(t, expectedIDs, actualIDs)

	// Create mapper to ensure everything works
	m, err := mapper.NewMapper(config.Lists)
	require.NoError(t, err)
	require.NotNil(t, m)

	// Test that the mapper actually works with a real transformation
	app := fiber.New()
	setupRoutes(app, m, config)

	// Test a transformation from example-mapper-1
	testInput := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, "/example-mapper-1/query?dir=atob", bytes.NewBufferString(testInput))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	// Verify the transformation was applied
	wrap := result["wrap"].(map[string]any)
	assert.Equal(t, "koral:termGroup", wrap["@type"])
	operands := wrap["operands"].([]any)
	require.Greater(t, len(operands), 0)
	firstOperand := operands[0].(map[string]any)
	assert.Equal(t, "DET", firstOperand["key"])
}

func TestConfigurableServiceURL(t *testing.T) {
	// Create test mapping list
	mappingList := tmconfig.MappingList{
		ID: "test-mapper",
		Mappings: []tmconfig.MappingRule{
			"[A] <> [B]",
		},
	}

	tests := []struct {
		name               string
		customServiceURL   string
		expectedServiceURL string
	}{
		{
			name:               "Custom service URL",
			customServiceURL:   "https://custom.example.com/plugin/koralmapper",
			expectedServiceURL: "https://custom.example.com/plugin/koralmapper",
		},
		{
			name:               "Default service URL when not specified",
			customServiceURL:   "", // Will use default
			expectedServiceURL: "https://korap.ids-mannheim.de/plugin/koralmapper",
		},
		{
			name:               "Custom service URL with different path",
			customServiceURL:   "https://my-server.org/api/v1/koralmapper",
			expectedServiceURL: "https://my-server.org/api/v1/koralmapper",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mapper
			m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
			require.NoError(t, err)

			// Create mock config with custom service URL
			mockConfig := &tmconfig.MappingConfig{
				ServiceURL: tt.customServiceURL,
				Lists:      []tmconfig.MappingList{mappingList},
			}

			// Apply defaults to simulate the real loading process
			tmconfig.ApplyDefaults(mockConfig)

			// Create fiber app
			app := fiber.New()
			setupRoutes(app, m, mockConfig)

			// Test Kalamar plugin endpoint with a specific mapID
			req := httptest.NewRequest(http.MethodGet, "/test-mapper", nil)
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, http.StatusOK, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			htmlContent := string(body)

			// html/template applies JS string escaping (/ -> \/, & -> \u0026)
			expectedJSURL := jsEscapeURL(tt.expectedServiceURL + "/test-mapper/query")
			assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)

			// Ensure it's still a valid HTML page
			assert.Contains(t, htmlContent, "Koral-Mapper")
			assert.Contains(t, htmlContent, "<!DOCTYPE html>")
		})
	}
}

func TestServiceURLConfigFileLoading(t *testing.T) {
	// Create a temporary config file with custom service URL
	configContent := `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
serviceURL: "https://custom.example.com/api/koralmapper"
lists:
- id: config-mapper
  mappings:
    - "[X] <> [Y]"
`
	configFile, err := os.CreateTemp("", "service-url-config-*.yaml")
	require.NoError(t, err)
	defer os.Remove(configFile.Name())

	_, err = configFile.WriteString(configContent)
	require.NoError(t, err)
	err = configFile.Close()
	require.NoError(t, err)

	// Load configuration from file
	config, err := tmconfig.LoadFromSources(configFile.Name(), nil)
	require.NoError(t, err)

	// Verify that the service URL was loaded correctly
	assert.Equal(t, "https://custom.example.com/api/koralmapper", config.ServiceURL)

	// Verify other fields are also preserved
	assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
	assert.Equal(t, "https://custom.example.com/", config.Server)

	// Create mapper and test the service URL is used in the HTML
	m, err := mapper.NewMapper(config.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, config)

	req := httptest.NewRequest(http.MethodGet, "/config-mapper", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	htmlContent := string(body)
	expectedJSURL := jsEscapeURL("https://custom.example.com/api/koralmapper/config-mapper/query")
	assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)
}

func TestServiceURLDefaults(t *testing.T) {
	// Test that defaults are applied correctly when creating a config
	config := &tmconfig.MappingConfig{
		Lists: []tmconfig.MappingList{
			{
				ID:       "test",
				Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
			},
		},
	}

	// Apply defaults (simulating what happens during loading)
	tmconfig.ApplyDefaults(config)

	// Check that the default service URL was applied
	assert.Equal(t, "https://korap.ids-mannheim.de/plugin/koralmapper", config.ServiceURL)

	// Check that other defaults were also applied
	assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)
	assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
	assert.Equal(t, "https://korap.ids-mannheim.de/css/kalamar-plugin-latest.css", config.Stylesheet)
	assert.Equal(t, 5725, config.Port)
	assert.Equal(t, "warn", config.LogLevel)
}

func TestServiceURLWithExampleConfig(t *testing.T) {
	// Test that the actual example config file works with the new serviceURL functionality
	// and that defaults are properly applied when serviceURL is not specified

	config, err := tmconfig.LoadFromSources("../../testdata/example-config.yaml", nil)
	require.NoError(t, err)

	// Verify that the default service URL was applied since it's not in the example config
	assert.Equal(t, "https://korap.ids-mannheim.de/plugin/koralmapper", config.ServiceURL)

	// Verify other values from the example config are preserved
	assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
	assert.Equal(t, "https://korap.ids-mannheim.de/css/kalamar-plugin-latest.css", config.Stylesheet)
	assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)

	// Verify the mapper was loaded correctly
	require.Len(t, config.Lists, 1)
	assert.Equal(t, "main-config-mapper", config.Lists[0].ID)

	// Create mapper and test that the service URL is used correctly in the HTML
	m, err := mapper.NewMapper(config.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, config)

	req := httptest.NewRequest(http.MethodGet, "/main-config-mapper", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	htmlContent := string(body)
	expectedJSURL := jsEscapeURL("https://korap.ids-mannheim.de/plugin/koralmapper/main-config-mapper/query")
	assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)
}

func TestBuildMapServiceURLWithURLJoining(t *testing.T) {
	tests := []struct {
		name       string
		serviceURL string
		mapID      string
		endpoint   string
		expected   string
	}{
		{
			name:       "Service URL without trailing slash",
			serviceURL: "https://example.com/plugin/koralmapper",
			mapID:      "test-mapper",
			endpoint:   "query",
			expected:   "https://example.com/plugin/koralmapper/test-mapper/query?dir=atob",
		},
		{
			name:       "Service URL with trailing slash",
			serviceURL: "https://example.com/plugin/koralmapper/",
			mapID:      "test-mapper",
			endpoint:   "query",
			expected:   "https://example.com/plugin/koralmapper/test-mapper/query?dir=atob",
		},
		{
			name:       "Map ID with leading slash",
			serviceURL: "https://example.com/plugin/koralmapper",
			mapID:      "/test-mapper",
			endpoint:   "query",
			expected:   "https://example.com/plugin/koralmapper/test-mapper/query?dir=atob",
		},
		{
			name:       "Both with slashes",
			serviceURL: "https://example.com/plugin/koralmapper/",
			mapID:      "/test-mapper",
			endpoint:   "query",
			expected:   "https://example.com/plugin/koralmapper/test-mapper/query?dir=atob",
		},
		{
			name:       "Complex map ID",
			serviceURL: "https://example.com/api/v1/",
			mapID:      "complex-mapper-name_123",
			endpoint:   "query",
			expected:   "https://example.com/api/v1/complex-mapper-name_123/query?dir=atob",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := buildMapServiceURL(tt.serviceURL, tt.mapID, tt.endpoint, QueryParams{
				Dir:      "atob",
				FoundryA: "",
				FoundryB: "",
				LayerA:   "",
				LayerB:   "",
			})
			require.NoError(t, err)
			assert.Equal(t, tt.expected, got)
		})
	}
}

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/koralmapper",
		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/koralmapper/test-mapper/query?dir=atob",
			expectedRespURL:  "https://example.com/plugin/koralmapper/test-mapper/response?dir=btoa",
			expectedStatus:   http.StatusOK,
		},
		{
			name:             "Explicit dir=atob",
			url:              "/test-mapper?dir=atob",
			expectedQueryURL: "https://example.com/plugin/koralmapper/test-mapper/query?dir=atob",
			expectedRespURL:  "https://example.com/plugin/koralmapper/test-mapper/response?dir=btoa",
			expectedStatus:   http.StatusOK,
		},
		{
			name:             "Explicit dir=btoa",
			url:              "/test-mapper?dir=btoa",
			expectedQueryURL: "https://example.com/plugin/koralmapper/test-mapper/query?dir=btoa",
			expectedRespURL:  "https://example.com/plugin/koralmapper/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/koralmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos",
			expectedRespURL:  "https://example.com/plugin/koralmapper/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/koralmapper/test-mapper/query?dir=btoa&layerA=pos&layerB=upos",
			expectedRespURL:  "https://example.com/plugin/koralmapper/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/koralmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
			expectedRespURL:  "https://example.com/plugin/koralmapper/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)

				// html/template applies JS string escaping in script contexts
				assert.Contains(t, htmlContent, "'service' : '"+jsEscapeURL(tt.expectedQueryURL)+"'")
				assert.Contains(t, htmlContent, "'service' : '"+jsEscapeURL(tt.expectedRespURL)+"'")

				// Ensure it's still a valid HTML page
				assert.Contains(t, htmlContent, "Koral-Mapper")
				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)
		})
	}
}

func TestCompositeQueryEndpoint(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: step1
    foundryA: opennlp
    layerA: p
    foundryB: stts
    layerB: p
    rewrites: true
    mappings:
      - "[PIDAT] <> [DET]"
  - id: step2
    foundryA: stts
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: true
    mappings:
      - "[DET] <> [PRON]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	tests := []struct {
		name         string
		url          string
		input        string
		expectedCode int
		expected     any
	}{
		{
			name:         "cascades two query mappings",
			url:          "/query/step1:atob;step2:atob",
			expectedCode: http.StatusOK,
			input: `{
				"@type": "koral:token",
				"wrap": {
					"@type": "koral:term",
					"foundry": "opennlp",
					"key": "PIDAT",
					"layer": "p",
					"match": "match:eq"
				}
			}`,
			expected: map[string]any{
				"@type": "koral:token",
				"wrap": map[string]any{
					"@type":   "koral:term",
					"foundry": "upos",
					"key":     "PRON",
					"layer":   "p",
					"match":   "match:eq",
					"rewrites": []any{
						map[string]any{
							"@type":  "koral:rewrite",
							"editor": "Koral-Mapper",
							"original": map[string]any{
								"@type":   "koral:term",
								"foundry": "opennlp",
								"key":     "PIDAT",
								"layer":   "p",
								"match":   "match:eq",
							},
						},
						map[string]any{
							"@type":  "koral:rewrite",
							"editor": "Koral-Mapper",
							"original": map[string]any{
								"@type":   "koral:term",
								"foundry": "stts",
								"key":     "DET",
								"layer":   "p",
								"match":   "match:eq",
							},
						},
					},
				},
			},
		},
		{
			name:         "invalid cfg returns bad request",
			url:          "/query/missing:atob",
			expectedCode: http.StatusBadRequest,
			input:        `{"@type": "koral:token"}`,
			expected: map[string]any{
				"error": `unknown mapping ID "missing"`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, tt.url, bytes.NewBufferString(tt.input))
			req.Header.Set("Content-Type", "application/json")

			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tt.expectedCode, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			var actual any
			err = json.Unmarshal(body, &actual)
			require.NoError(t, err)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestCompositeResponseEndpoint(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: resp-step1
    type: corpus
    mappings:
      - "textClass=novel <> genre=fiction"
  - id: resp-step2
    type: corpus
    mappings:
      - "genre=fiction <> category=lit"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	tests := []struct {
		name         string
		url          string
		input        string
		expectedCode int
		assertBody   func(t *testing.T, actual map[string]any)
	}{
		{
			name:         "cascades two response mappings",
			url:          "/response/resp-step1:atob;resp-step2:atob",
			expectedCode: http.StatusOK,
			input: `{
				"fields": [{
					"@type": "koral:field",
					"key": "textClass",
					"value": "novel",
					"type": "type:string"
				}]
			}`,
			assertBody: func(t *testing.T, actual map[string]any) {
				fields := actual["fields"].([]any)
				require.Len(t, fields, 3)
				assert.Equal(t, "textClass", fields[0].(map[string]any)["key"])
				assert.Equal(t, "genre", fields[1].(map[string]any)["key"])
				assert.Equal(t, "fiction", fields[1].(map[string]any)["value"])
				assert.Equal(t, "category", fields[2].(map[string]any)["key"])
				assert.Equal(t, "lit", fields[2].(map[string]any)["value"])
			},
		},
		{
			name:         "invalid cfg returns bad request",
			url:          "/response/resp-step1",
			expectedCode: http.StatusBadRequest,
			input:        `{"fields": []}`,
			assertBody: func(t *testing.T, actual map[string]any) {
				assert.Contains(t, actual["error"], "expected at least 2 colon-separated fields")
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, tt.url, bytes.NewBufferString(tt.input))
			req.Header.Set("Content-Type", "application/json")

			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tt.expectedCode, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			var actual map[string]any
			err = json.Unmarshal(body, &actual)
			require.NoError(t, err)
			tt.assertBody(t, actual)
		})
	}
}

func TestEmbeddedFilesExist(t *testing.T) {
	files := []string{"static/config.html", "static/plugin.html", "static/config.js", "static/style.css"}
	for _, f := range files {
		t.Run(f, func(t *testing.T) {
			data, err := fs.ReadFile(staticFS, f)
			require.NoError(t, err, "embedded file %s should exist", f)
			assert.NotEmpty(t, data, "embedded file %s should not be empty", f)
		})
	}
}

func TestConfigTemplateParsesSuccessfully(t *testing.T) {
	tmpl, err := template.ParseFS(staticFS, "static/config.html")
	require.NoError(t, err, "config template should parse without error")
	require.NotNil(t, tmpl)
}

func TestStaticFileServing(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}
	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)
	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	tests := []struct {
		name          string
		url           string
		expectedCode  int
		expectedCType string
	}{
		{
			name:          "config.js is served with correct content type",
			url:           "/static/config.js",
			expectedCode:  http.StatusOK,
			expectedCType: "text/javascript",
		},
		{
			name:          "style.css is served with correct content type",
			url:           "/static/style.css",
			expectedCode:  http.StatusOK,
			expectedCType: "text/css",
		},
		{
			name:         "non-existent static file returns 404",
			url:          "/static/nonexistent.txt",
			expectedCode: http.StatusNotFound,
		},
	}

	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.expectedCode, resp.StatusCode)
			if tt.expectedCType != "" {
				assert.Contains(t, resp.Header.Get("Content-Type"), tt.expectedCType)
			}
		})
	}
}

func TestStaticFileContent(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}
	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)
	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// Verify served content matches embedded content
	files := []string{"config.js", "style.css"}
	for _, f := range files {
		t.Run(f, func(t *testing.T) {
			embedded, err := fs.ReadFile(staticFS, "static/"+f)
			require.NoError(t, err)

			req := httptest.NewRequest(http.MethodGet, "/static/"+f, nil)
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			assert.Equal(t, embedded, body, "served content should match embedded content for %s", f)
		})
	}
}

func TestConfigPageRendering(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:          "anno-mapper",
			Description: "Annotation mapping",
			FoundryA:    "opennlp",
			LayerA:      "p",
			FoundryB:    "upos",
			LayerB:      "p",
			Mappings:    []tmconfig.MappingRule{"[A] <> [B]"},
		},
		{
			ID:          "corpus-mapper",
			Type:        "corpus",
			Description: "Corpus mapping",
			FieldA:      "wikiCat",
			FieldB:      "textClass",
			Mappings:    []tmconfig.MappingRule{"textClass=science <> textClass=akademisch"},
		},
	}
	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		SDK:        "https://example.com/sdk.js",
		Stylesheet: "https://example.com/kalamar.css",
		Server:     "https://example.com/",
		ServiceURL: "https://example.com/plugin/koralmapper",
		Lists:      lists,
	}

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// HTML structure
	assert.Contains(t, htmlContent, "<!DOCTYPE html>")
	assert.Contains(t, htmlContent, `<meta charset="UTF-8">`)
	assert.Contains(t, htmlContent, "Koral-Mapper")

	// SDK and server
	assert.Contains(t, htmlContent, `src="https://example.com/sdk.js"`)
	assert.Contains(t, htmlContent, `href="https://example.com/kalamar.css"`)
	assert.Contains(t, htmlContent, `data-server="https://example.com/"`)

	// ServiceURL as data attribute
	assert.Contains(t, htmlContent, `data-service-url="https://example.com/plugin/koralmapper"`)

	// Static file references
	assert.Contains(t, htmlContent, `static/style.css`)
	assert.Contains(t, htmlContent, `static/config.js`)

	// Request/response sections
	assert.Contains(t, htmlContent, "<legend>Request</legend>")
	assert.Contains(t, htmlContent, "<legend>Response</legend>")

	// Annotation mapping entries
	assert.Contains(t, htmlContent, "query")
	assert.Contains(t, htmlContent, "anno-mapper")
	assert.Contains(t, htmlContent, `data-id="anno-mapper"`)
	assert.Contains(t, htmlContent, `data-type="annotation"`)
	assert.Contains(t, htmlContent, `placeholder="opennlp"`)
	assert.Contains(t, htmlContent, `placeholder="upos"`)
	assert.Contains(t, htmlContent, "Annotation mapping")

	// Corpus mapping entries
	assert.Contains(t, htmlContent, "corpus")
	assert.Contains(t, htmlContent, "<strong>corpus-mapper</strong>")
	assert.Contains(t, htmlContent, `data-id="corpus-mapper"`)
	assert.Contains(t, htmlContent, `data-type="corpus"`)
	assert.Contains(t, htmlContent, `data-default-field-a="wikiCat"`)
	assert.Contains(t, htmlContent, `data-default-field-b="textClass"`)
	assert.Contains(t, htmlContent, `class="request-fieldA"`)
	assert.Contains(t, htmlContent, `class="request-fieldB"`)
	assert.Contains(t, htmlContent, `class="response-fieldA"`)
	assert.Contains(t, htmlContent, `class="response-fieldB"`)
	assert.Contains(t, htmlContent, "Corpus mapping")

	// Reset button
	assert.Contains(t, htmlContent, `id="reset-btn"`)
	assert.Contains(t, htmlContent, "Reset all")
}

func TestConfigPageAnnotationMappingHasFoundryInputs(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "anno-mapper",
			FoundryA: "opennlp",
			LayerA:   "p",
			FoundryB: "upos",
			LayerB:   "pos",
			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
		},
	}
	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: lists}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// Data attributes for default values
	assert.Contains(t, htmlContent, `data-default-foundry-a="opennlp"`)
	assert.Contains(t, htmlContent, `data-default-layer-a="p"`)
	assert.Contains(t, htmlContent, `data-default-foundry-b="upos"`)
	assert.Contains(t, htmlContent, `data-default-layer-b="pos"`)

	// Input fields with request/response-specific CSS classes
	assert.Contains(t, htmlContent, `class="request-foundryA"`)
	assert.Contains(t, htmlContent, `class="request-layerA"`)
	assert.Contains(t, htmlContent, `class="request-foundryB"`)
	assert.Contains(t, htmlContent, `class="request-layerB"`)
	assert.Contains(t, htmlContent, `class="response-foundryA"`)
	assert.Contains(t, htmlContent, `class="response-layerA"`)
	assert.Contains(t, htmlContent, `class="response-foundryB"`)
	assert.Contains(t, htmlContent, `class="response-layerB"`)

	// Direction arrows are independent for request/response
	assert.Contains(t, htmlContent, `class="request-dir-arrow"`)
	assert.Contains(t, htmlContent, `class="response-dir-arrow"`)
	assert.Contains(t, htmlContent, `data-dir="atob"`)
	assert.Contains(t, htmlContent, `data-dir="btoa"`)

	// Request and response checkboxes
	assert.Contains(t, htmlContent, `class="checkbox request-cb"`)
	assert.Contains(t, htmlContent, `class="checkbox response-cb"`)
}

func TestConfigPageCorpusMappingHasFieldAndDirectionInputs(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "corpus-mapper",
			Type:     "corpus",
			FieldA:   "genre",
			FieldB:   "topic",
			Mappings: []tmconfig.MappingRule{"textClass=science <> textClass=akademisch"},
		},
	}

	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: lists}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// Corpus section exists
	assert.Contains(t, htmlContent, `data-id="corpus-mapper"`)
	assert.Contains(t, htmlContent, `data-type="corpus"`)

	// Checkboxes present
	assert.Contains(t, htmlContent, `class="checkbox request-cb"`)
	assert.Contains(t, htmlContent, `class="checkbox response-cb"`)

	// No annotation foundry/layer inputs (only corpus mappings)
	assert.NotContains(t, htmlContent, `class="request-foundryA"`)
	assert.NotContains(t, htmlContent, `class="request-layerA"`)
	assert.Contains(t, htmlContent, `class="request-dir-arrow"`)
	assert.Contains(t, htmlContent, `class="response-dir-arrow"`)
	assert.Contains(t, htmlContent, `class="request-fieldA"`)
	assert.Contains(t, htmlContent, `class="request-fieldB"`)
	assert.Contains(t, htmlContent, `class="response-fieldA"`)
	assert.Contains(t, htmlContent, `class="response-fieldB"`)
	assert.Contains(t, htmlContent, `placeholder="genre"`)
	assert.Contains(t, htmlContent, `placeholder="topic"`)
}

// TestPluginPageEscapesMapID verifies that the plugin page (GET /:map)
// properly HTML-escapes the map ID to prevent XSS via URL path injection.
func TestPluginPageEscapesMapID(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		ServiceURL: "https://example.com/plugin/koralmapper",
		Lists:      []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// Use a map ID that contains HTML/JS injection payload
	maliciousMapID := `"><script>alert(1)</script>`
	req := httptest.NewRequest(http.MethodGet, "/"+maliciousMapID, nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	// The request may fail validation (contains invalid chars), which is also acceptable.
	// If it renders, the output must not contain unescaped script tags.
	if resp.StatusCode == http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		htmlContent := string(body)

		// The raw script tag must NOT appear in the output
		assert.NotContains(t, htmlContent, "<script>alert(1)</script>")
		// If rendered, it should be escaped
		assert.Contains(t, htmlContent, "&lt;script&gt;")
	}
}

// TestPluginPageEscapesServiceURL verifies that the plugin page properly
// escapes ServiceURL values to prevent template injection.
func TestPluginPageEscapesServiceURL(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	// Inject a ServiceURL with HTML-special characters
	mockConfig := &tmconfig.MappingConfig{
		ServiceURL: `https://example.com/plugin" onload="alert(1)`,
		Lists:      []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/test-mapper", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// The unescaped injection must NOT appear in the output
	assert.NotContains(t, htmlContent, `onload="alert(1)"`)
}

func TestConfigPageBackwardCompatibility(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "test-mapper",
			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
		},
	}

	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		ServiceURL: "https://example.com/plugin/koralmapper",
		Lists:      lists,
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/test-mapper", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// Old-style single-mapping page behavior
	assert.Contains(t, htmlContent, "<!DOCTYPE html>")
	assert.Contains(t, htmlContent, "Koral-Mapper")
	assert.Contains(t, htmlContent, "Map ID: test-mapper")
	assert.Contains(t, htmlContent, "KorAPlugin.sendMsg")
	assert.Contains(t, htmlContent, `test-mapper\/query`)
}

func TestBuildConfigPageData(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:          "anno1",
			Description: "First annotation",
			FoundryA:    "f1a",
			LayerA:      "l1a",
			FoundryB:    "f1b",
			LayerB:      "l1b",
			Mappings:    []tmconfig.MappingRule{"[A] <> [B]"},
		},
		{
			ID:          "corpus1",
			Type:        "corpus",
			Description: "First corpus",
			Mappings:    []tmconfig.MappingRule{"textClass=a <> textClass=b"},
		},
		{
			ID:          "anno2",
			Type:        "annotation",
			Description: "Second annotation",
			FoundryA:    "f2a",
			LayerA:      "l2a",
			FoundryB:    "f2b",
			LayerB:      "l2b",
			Mappings:    []tmconfig.MappingRule{"[C] <> [D]"},
		},
	}

	mockConfig := &tmconfig.MappingConfig{
		SDK:        "https://example.com/sdk.js",
		Stylesheet: "https://example.com/kalamar.css",
		Server:     "https://example.com/",
		ServiceURL: "https://example.com/service",
		Lists:      lists,
	}

	data := buildConfigPageData(mockConfig)

	assert.Equal(t, "https://example.com/sdk.js", data.SDK)
	assert.Equal(t, "https://example.com/kalamar.css", data.Stylesheet)
	assert.Equal(t, "https://example.com/", data.Server)
	assert.Equal(t, "https://example.com/service", data.ServiceURL)

	require.Len(t, data.AnnotationMappings, 2)
	assert.Equal(t, "anno1", data.AnnotationMappings[0].ID)
	assert.Equal(t, "annotation", data.AnnotationMappings[0].Type)
	assert.Equal(t, "f1a", data.AnnotationMappings[0].FoundryA)
	assert.Equal(t, "First annotation", data.AnnotationMappings[0].Description)
	assert.Equal(t, "anno2", data.AnnotationMappings[1].ID)
	assert.Equal(t, "annotation", data.AnnotationMappings[1].Type)

	require.Len(t, data.CorpusMappings, 1)
	assert.Equal(t, "corpus1", data.CorpusMappings[0].ID)
	assert.Equal(t, "corpus", data.CorpusMappings[0].Type)
	assert.Equal(t, "First corpus", data.CorpusMappings[0].Description)
}

func TestConfigPageDefaultsAsPlaceholdersOnly(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "anno-mapper",
			FoundryA: "opennlp",
			LayerA:   "p",
			FoundryB: "upos",
			LayerB:   "pos",
			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
		},
		{
			ID:     "corpus-mapper",
			Type:   "corpus",
			FieldA: "genre",
			FieldB: "topic",
			Mappings: []tmconfig.MappingRule{
				"textClass=science <> textClass=akademisch",
			},
		},
	}
	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: lists}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// Placeholders are present
	assert.Contains(t, htmlContent, `placeholder="opennlp"`)
	assert.Contains(t, htmlContent, `placeholder="upos"`)
	assert.Contains(t, htmlContent, `placeholder="genre"`)
	assert.Contains(t, htmlContent, `placeholder="topic"`)

	// Value attributes must NOT appear on mapping inputs (defaults shown as
	// placeholders only). We check that the combined string value="opennlp"
	// etc. is absent.
	assert.NotContains(t, htmlContent, `value="opennlp"`)
	assert.NotContains(t, htmlContent, `value="upos"`)
	assert.NotContains(t, htmlContent, `value="genre"`)
	assert.NotContains(t, htmlContent, `value="topic"`)
}

func TestConfigPageHasResetButton(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "test-mapper",
			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
		},
	}
	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: lists}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	assert.Contains(t, htmlContent, `id="reset-btn"`)
	assert.Contains(t, htmlContent, "Reset all")
	assert.Contains(t, htmlContent, `type="button"`)
}

func TestAddRewritesDefaultOff(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob", bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap := result["wrap"].(map[string]any)
	assert.Equal(t, "DET", wrap["key"])
	assert.Nil(t, wrap["rewrites"], "rewrites should not be present by default")
}

func TestAddRewritesEnabledViaYAML(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: true
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob", bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap := result["wrap"].(map[string]any)
	assert.Equal(t, "DET", wrap["key"])
	assert.NotNil(t, wrap["rewrites"], "rewrites should be present when rewrites are enabled in YAML")
}

func TestAddRewritesQueryParamOverridesYAML(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: true
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	// Override YAML rewrites=true with query param rewrites=false
	req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob&rewrites=false", bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap := result["wrap"].(map[string]any)
	assert.Equal(t, "DET", wrap["key"])
	assert.Nil(t, wrap["rewrites"], "rewrites should be suppressed by query param override")
}

func TestAddRewritesQueryParamEnablesWhenYAMLOff(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob&rewrites=true", bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap := result["wrap"].(map[string]any)
	assert.Equal(t, "DET", wrap["key"])
	assert.NotNil(t, wrap["rewrites"], "rewrites should be present when enabled by query param")
}

// TestSecurityHeadersPresent verifies that all HTTP responses include
// security headers to mitigate MIME-sniffing and referrer leaks.
// X-Frame-Options is intentionally NOT set because the service is
// embedded in cross-origin iframes as a Kalamar plugin.
func TestSecurityHeadersPresent(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	endpoints := []struct {
		method string
		url    string
		body   string
	}{
		{http.MethodGet, "/health", ""},
		{http.MethodGet, "/", ""},
		{http.MethodPost, "/test-mapper/query?dir=atob", `{"@type":"koral:token","wrap":{"@type":"koral:term","foundry":"x","key":"A","layer":"p","match":"match:eq"}}`},
	}

	for _, ep := range endpoints {
		t.Run(ep.method+" "+ep.url, func(t *testing.T) {
			var req *http.Request
			if ep.body != "" {
				req = httptest.NewRequest(ep.method, ep.url, bytes.NewBufferString(ep.body))
				req.Header.Set("Content-Type", "application/json")
			} else {
				req = httptest.NewRequest(ep.method, ep.url, nil)
			}

			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"),
				"X-Content-Type-Options header must be set to nosniff")
			assert.Equal(t, "strict-origin-when-cross-origin", resp.Header.Get("Referrer-Policy"),
				"Referrer-Policy header must be set")
			assert.Empty(t, resp.Header.Get("X-Frame-Options"),
				"X-Frame-Options must NOT be set (cross-origin iframe embedding)")
		})
	}
}

// TestRateLimitingEnforced verifies that the server applies per-client rate
// limiting and returns HTTP 429 when the limit is exceeded.
func TestRateLimitingEnforced(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// The default rate limit is 100/min. Send more requests than that
	// to verify enforcement.
	var lastStatus int
	exceeded := false
	for range 150 {
		req := httptest.NewRequest(http.MethodGet, "/health", nil)
		resp, err := app.Test(req)
		require.NoError(t, err)
		resp.Body.Close()
		lastStatus = resp.StatusCode
		if lastStatus == fiber.StatusTooManyRequests {
			exceeded = true
			break
		}
	}

	assert.True(t, exceeded, "rate limiter should return 429 when limit is exceeded, last status was %d", lastStatus)
}

// TestRateLimitingConfigurable verifies that the rate limit can be customized
// via the rateLimit configuration field.
func TestRateLimitingConfigurable(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	// Set a very low rate limit to make testing fast
	mockConfig := &tmconfig.MappingConfig{
		RateLimit: 5,
		Lists:     []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// With a limit of 5, the 6th request should be rejected
	for i := range 5 {
		req := httptest.NewRequest(http.MethodGet, "/health", nil)
		resp, err := app.Test(req)
		require.NoError(t, err)
		resp.Body.Close()
		assert.Equal(t, http.StatusOK, resp.StatusCode,
			"request %d should succeed within the rate limit", i+1)
	}

	// The next request should exceed the limit
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	resp.Body.Close()
	assert.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode,
		"request beyond the configured limit should return 429")
}

// TestRateLimitDefaultValue verifies the default rate limit is 100.
func TestRateLimitDefaultValue(t *testing.T) {
	cfg := &tmconfig.MappingConfig{}
	tmconfig.ApplyDefaults(cfg)
	assert.Equal(t, 100, cfg.RateLimit, "default rate limit should be 100 requests per minute")
}

func TestConfigPagePreservesOrderOfMappings(t *testing.T) {
	lists := []tmconfig.MappingList{
		{
			ID:       "mapper-z",
			FoundryA: "fa",
			FoundryB: "fb",
			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
		},
		{
			ID:       "mapper-a",
			FoundryA: "fa",
			FoundryB: "fb",
			Mappings: []tmconfig.MappingRule{"[C] <> [D]"},
		},
		{
			ID:       "mapper-m",
			FoundryA: "fa",
			FoundryB: "fb",
			Mappings: []tmconfig.MappingRule{"[E] <> [F]"},
		},
	}
	m, err := mapper.NewMapper(lists)
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: lists}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	htmlContent := string(body)

	// Verify the order is preserved (z before a before m)
	idxZ := strings.Index(htmlContent, `data-id="mapper-z"`)
	idxA := strings.Index(htmlContent, `data-id="mapper-a"`)
	idxM := strings.Index(htmlContent, `data-id="mapper-m"`)
	assert.Greater(t, idxA, idxZ, "mapper-a should appear after mapper-z")
	assert.Greater(t, idxM, idxA, "mapper-m should appear after mapper-a")
}

// TestCORSHeadersDefault verifies that CORS headers are present with
// the default AllowOrigins value when no custom origin is configured.
func TestCORSHeadersDefault(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	// Preflight OPTIONS request with default allowed origin
	req := httptest.NewRequest(http.MethodOptions, "/health", nil)
	req.Header.Set("Origin", "https://korap.ids-mannheim.de")
	req.Header.Set("Access-Control-Request-Method", "GET")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, "https://korap.ids-mannheim.de",
		resp.Header.Get("Access-Control-Allow-Origin"),
		"default AllowOrigins should include korap.ids-mannheim.de")
	assert.NotEmpty(t, resp.Header.Get("Access-Control-Allow-Methods"))

	// Actual GET request should include CORS headers too
	req = httptest.NewRequest(http.MethodGet, "/health", nil)
	req.Header.Set("Origin", "https://korap.ids-mannheim.de")
	resp, err = app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "https://korap.ids-mannheim.de",
		resp.Header.Get("Access-Control-Allow-Origin"))
}

// TestCORSHeadersCustomOrigin verifies that a custom AllowOrigins value
// is propagated to CORS response headers.
func TestCORSHeadersCustomOrigin(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		AllowOrigins: "https://custom.example.com",
		Lists:        []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	req.Header.Set("Origin", "https://custom.example.com")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "https://custom.example.com",
		resp.Header.Get("Access-Control-Allow-Origin"),
		"custom AllowOrigins should be reflected in the response")
}

// TestCORSRejectsDisallowedOrigin verifies that requests from origins
// not listed in AllowOrigins do not receive an Access-Control-Allow-Origin
// header.
func TestCORSRejectsDisallowedOrigin(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		AllowOrigins: "https://allowed.example.com",
		Lists:        []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	req.Header.Set("Origin", "https://evil.example.com")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Empty(t, resp.Header.Get("Access-Control-Allow-Origin"),
		"disallowed origin must not receive Access-Control-Allow-Origin")
}

// TestCORSAllowsMultipleOrigins verifies that multiple comma-separated
// origins are all accepted by the CORS middleware.
func TestCORSAllowsMultipleOrigins(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{
		AllowOrigins: "https://first.example.com,https://second.example.com",
		Lists:        []tmconfig.MappingList{mappingList},
	}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	for _, origin := range []string{
		"https://first.example.com",
		"https://second.example.com",
	} {
		t.Run(origin, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/health", nil)
			req.Header.Set("Origin", origin)
			resp, err := app.Test(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, http.StatusOK, resp.StatusCode)
			assert.Equal(t, origin,
				resp.Header.Get("Access-Control-Allow-Origin"),
				"both configured origins should be accepted")
		})
	}
}

// TestCORSPreflightAllowedMethods verifies that the CORS preflight
// response advertises only GET and POST methods by default.
func TestCORSPreflightAllowedMethods(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodOptions, "/test-mapper/query", nil)
	req.Header.Set("Origin", "https://korap.ids-mannheim.de")
	req.Header.Set("Access-Control-Request-Method", "POST")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	allowedMethods := resp.Header.Get("Access-Control-Allow-Methods")
	assert.Contains(t, allowedMethods, "GET")
	assert.Contains(t, allowedMethods, "POST")
}

// TestCORSPreflightAllowedHeaders verifies that the Content-Type
// header is allowed in CORS preflight responses.
func TestCORSPreflightAllowedHeaders(t *testing.T) {
	mappingList := tmconfig.MappingList{
		ID:       "test-mapper",
		Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
	}

	m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
	require.NoError(t, err)

	mockConfig := &tmconfig.MappingConfig{Lists: []tmconfig.MappingList{mappingList}}
	tmconfig.ApplyDefaults(mockConfig)

	app := fiber.New()
	setupRoutes(app, m, mockConfig)

	req := httptest.NewRequest(http.MethodOptions, "/test-mapper/query", nil)
	req.Header.Set("Origin", "https://korap.ids-mannheim.de")
	req.Header.Set("Access-Control-Request-Method", "POST")
	req.Header.Set("Access-Control-Request-Headers", "Content-Type")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	allowedHeaders := resp.Header.Get("Access-Control-Allow-Headers")
	assert.Contains(t, allowedHeaders, "Content-Type")
}

// TestCORSOnTransformEndpoint verifies CORS headers are present on
// actual POST requests to the transformation endpoints (not just
// health check).
func TestCORSOnTransformEndpoint(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)

	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob",
		bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Origin", "https://korap.ids-mannheim.de")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "https://korap.ids-mannheim.de",
		resp.Header.Get("Access-Control-Allow-Origin"),
		"transform endpoints should include CORS headers")
}

// Helper for rewrites tests: sends a POST to the given URL with a standard input
// and returns whether the result's wrap contains a "rewrites" field.
func hasRewritesInResponse(t *testing.T, app *fiber.App, url string) bool {
	t.Helper()
	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "opennlp",
			"key": "PIDAT",
			"layer": "p",
			"match": "match:eq"
		}
	}`

	req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap, ok := result["wrap"].(map[string]any)
	require.True(t, ok, "result should have a wrap field")
	_, hasRewrites := wrap["rewrites"]
	return hasRewrites
}

func TestGlobalRewritesTrueInheritsToList(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
		"global rewrites=true should be inherited by list without per-list override")
}

func TestGlobalRewritesTrueOverriddenByListFalse(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: false
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.False(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
		"per-list rewrites=false should override global rewrites=true")
}

func TestGlobalRewritesFalseOverriddenByListTrue(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: true
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
		"per-list rewrites=true should override global rewrites=false (default)")
}

func TestQueryParamOverridesGlobalRewritesTrue(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.False(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob&rewrites=false"),
		"query param rewrites=false should override global rewrites=true")
}

func TestQueryParamOverridesPerListRewritesFalse(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-mapper
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: false
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob&rewrites=true"),
		"query param rewrites=true should override per-list rewrites=false")
}

func TestCompositeQueryGlobalRewritesInherited(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: step1
    foundryA: opennlp
    layerA: p
    foundryB: stts
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.True(t, hasRewritesInResponse(t, app, "/query/step1:atob"),
		"composite query should inherit global rewrites=true")
}

func TestCompositeQueryGlobalRewritesOverriddenByQueryParam(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: step1
    foundryA: opennlp
    layerA: p
    foundryB: stts
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.False(t, hasRewritesInResponse(t, app, "/query/step1:atob?rewrites=false"),
		"composite query param rewrites=false should override global rewrites=true")
}

func TestCompositeQueryPerListOverridesGlobal(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: step1
    foundryA: opennlp
    layerA: p
    foundryB: stts
    layerB: p
    rewrites: false
    mappings:
      - "[PIDAT] <> [DET]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	assert.False(t, hasRewritesInResponse(t, app, "/query/step1:atob"),
		"composite query should use per-list rewrites=false over global rewrites=true")
}

func TestResponseEndpointGlobalRewritesInherited(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-response-mapper
    foundryA: marmot
    layerA: m
    foundryB: opennlp
    layerB: p
    mappings:
      - "[gender:masc] <> [p=M & m=M]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	// Response rewrites on snippets don't use koral:rewrite fields the same way
	// but we can verify the handler doesn't error and respects the flag.
	input := `{"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"}`
	req := httptest.NewRequest(http.MethodPost, "/test-response-mapper/response?dir=atob",
		bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()
	assert.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestResponseEndpointQueryParamOverridesGlobal(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: test-response-mapper
    foundryA: marmot
    layerA: m
    foundryB: opennlp
    layerB: p
    mappings:
      - "[gender:masc] <> [p=M & m=M]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	input := `{"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"}`
	req := httptest.NewRequest(http.MethodPost, "/test-response-mapper/response?dir=atob&rewrites=false",
		bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()
	assert.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestMultipleListsMixedRewritesResolution(t *testing.T) {
	cfg := loadConfigFromYAML(t, `
rewrites: true
lists:
  - id: mapper-inherits
    foundryA: opennlp
    layerA: p
    foundryB: upos
    layerB: p
    mappings:
      - "[PIDAT] <> [DET]"
  - id: mapper-overrides-off
    foundryA: stts
    layerA: p
    foundryB: upos
    layerB: p
    rewrites: false
    mappings:
      - "[DET] <> [PRON]"
`)
	m, err := mapper.NewMapper(cfg.Lists)
	require.NoError(t, err)

	app := fiber.New()
	setupRoutes(app, m, cfg)

	// mapper-inherits should have rewrites (inherits global true)
	assert.True(t, hasRewritesInResponse(t, app, "/mapper-inherits/query?dir=atob"),
		"mapper-inherits should have rewrites via global default")

	// mapper-overrides-off has per-list rewrites=false
	input := `{
		"@type": "koral:token",
		"wrap": {
			"@type": "koral:term",
			"foundry": "stts",
			"key": "DET",
			"layer": "p",
			"match": "match:eq"
		}
	}`
	req := httptest.NewRequest(http.MethodPost, "/mapper-overrides-off/query?dir=atob",
		bytes.NewBufferString(input))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	var result map[string]any
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	wrap := result["wrap"].(map[string]any)
	_, hasRewrites := wrap["rewrites"]
	assert.False(t, hasRewrites,
		"mapper-overrides-off should NOT have rewrites (per-list false overrides global true)")
}
