package config

import (
	"bytes"
	"os"
	"path/filepath"
	"testing"

	"github.com/KorAP/Koral-Mapper/ast"
	"github.com/KorAP/Koral-Mapper/parser"
	"github.com/rs/zerolog/log"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestLoadConfig(t *testing.T) {
	// Create a temporary YAML file
	content := `
- id: opennlp-mapper
  foundryA: opennlp
  layerA: p
  foundryB: upos
  layerB: p
  mappings:
    - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
    - "[PAV] <> [ADV & PronType:Dem]"

- id: simple-mapper
  mappings:
    - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	err = tmpfile.Close()
	require.NoError(t, err)

	// Test loading the configuration
	config, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)

	// Verify the configuration
	require.Len(t, config.Lists, 2)

	// Check first mapping list
	list1 := config.Lists[0]
	assert.Equal(t, "opennlp-mapper", list1.ID)
	assert.Equal(t, "opennlp", list1.FoundryA)
	assert.Equal(t, "p", list1.LayerA)
	assert.Equal(t, "upos", list1.FoundryB)
	assert.Equal(t, "p", list1.LayerB)
	require.Len(t, list1.Mappings, 2)
	assert.Equal(t, "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]", string(list1.Mappings[0]))
	assert.Equal(t, "[PAV] <> [ADV & PronType:Dem]", string(list1.Mappings[1]))

	// Check second mapping list
	list2 := config.Lists[1]
	assert.Equal(t, "simple-mapper", list2.ID)
	assert.Empty(t, list2.FoundryA)
	assert.Empty(t, list2.LayerA)
	assert.Empty(t, list2.FoundryB)
	assert.Empty(t, list2.LayerB)
	require.Len(t, list2.Mappings, 1)
	assert.Equal(t, "[A] <> [B]", string(list2.Mappings[0]))
}

func TestParseMappings(t *testing.T) {
	list := &MappingList{
		ID:       "test-mapper",
		FoundryA: "opennlp",
		LayerA:   "p",
		FoundryB: "upos",
		LayerB:   "p",
		Mappings: []MappingRule{
			"[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
		},
	}

	results, err := list.ParseMappings()
	require.NoError(t, err)
	require.Len(t, results, 1)

	// Check the parsed upper pattern
	upper := results[0].Upper
	require.NotNil(t, upper)
	require.IsType(t, &ast.Token{}, upper)
	upperTerm := upper.Wrap.(*ast.Term)
	assert.Equal(t, "opennlp", upperTerm.Foundry)
	assert.Equal(t, "p", upperTerm.Layer)
	assert.Equal(t, "PIDAT", upperTerm.Key)

	// Check the parsed lower pattern
	lower := results[0].Lower
	require.NotNil(t, lower)
	require.IsType(t, &ast.Token{}, lower)
	lowerGroup := lower.Wrap.(*ast.TermGroup)
	require.Len(t, lowerGroup.Operands, 2)
	assert.Equal(t, ast.AndRelation, lowerGroup.Relation)

	// Check first operand
	term1 := lowerGroup.Operands[0].(*ast.Term)
	assert.Equal(t, "opennlp", term1.Foundry)
	assert.Equal(t, "p", term1.Layer)
	assert.Equal(t, "PIDAT", term1.Key)

	// Check second operand
	term2 := lowerGroup.Operands[1].(*ast.Term)
	assert.Equal(t, "opennlp", term2.Foundry)
	assert.Equal(t, "p", term2.Layer)
	assert.Equal(t, "AdjType", term2.Key)
	assert.Equal(t, "Pdt", term2.Value)
}

func TestLoadConfigValidation(t *testing.T) {
	tests := []struct {
		name    string
		content string
		wantErr string
	}{
		{
			name: "Missing ID",
			content: `
- foundryA: opennlp
  mappings:
    - "[A] <> [B]"
`,
			wantErr: "mapping list at index 0 is missing an ID",
		},
		{
			name: "Empty mappings",
			content: `
- id: test
  foundryA: opennlp
  mappings: []
`,
			wantErr: "mapping list 'test' has no mapping rules",
		},
		{
			name: "Empty rule",
			content: `
- id: test
  mappings:
    - ""
`,
			wantErr: "mapping list 'test' rule at index 0 is empty",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpfile, err := os.CreateTemp("", "config-*.yaml")
			require.NoError(t, err)
			defer os.Remove(tmpfile.Name())

			_, err = tmpfile.WriteString(tt.content)
			require.NoError(t, err)
			err = tmpfile.Close()
			require.NoError(t, err)

			_, err = LoadFromSources(tmpfile.Name(), nil)
			require.Error(t, err)
			assert.Contains(t, err.Error(), tt.wantErr)
		})
	}
}

func TestLoadConfigEdgeCases(t *testing.T) {
	tests := []struct {
		name    string
		content string
		wantErr string
	}{
		{
			name: "Duplicate mapping list IDs",
			content: `
- id: test
  mappings:
    - "[A] <> [B]"
- id: test
  mappings:
    - "[C] <> [D]"`,
			wantErr: "duplicate mapping list ID found: test",
		},
		{
			name: "Invalid YAML syntax",
			content: `
- id: test
  mappings:
    - [A] <> [B]  # Unquoted special characters
`,
			wantErr: "yaml",
		},
		{
			name:    "Empty file",
			content: "",
			wantErr: "EOF",
		},
		{
			name: "Non-list YAML",
			content: `
id: test
mappings:
  - "[A] <> [B]"`,
			wantErr: "no mapping lists found",
		},
		{
			name: "Missing required fields",
			content: `
- mappings:
    - "[A] <> [B]"
- id: test2
  foundryA: opennlp`,
			wantErr: "missing an ID",
		},
		{
			name: "Empty mappings list",
			content: `
- id: test
  foundryA: opennlp
  mappings: []`,
			wantErr: "has no mapping rules",
		},
		{
			name: "Null values in optional fields",
			content: `
- id: test
  foundryA: null
  layerA: null
  foundryB: null
  layerB: null
  mappings:
    - "[A] <> [B]"`,
			wantErr: "",
		},
		{
			name: "Special characters in IDs",
			content: `
- id: "test/special@chars#1"
  mappings:
    - "[A] <> [B]"`,
			wantErr: "",
		},
		{
			name: "Unicode characters in mappings",
			content: `
- id: test
  mappings:
    - "[ß] <> [ss]"
    - "[é] <> [e]"`,
			wantErr: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpfile, err := os.CreateTemp("", "config-*.yaml")
			require.NoError(t, err)
			defer os.Remove(tmpfile.Name())

			_, err = tmpfile.WriteString(tt.content)
			require.NoError(t, err)
			err = tmpfile.Close()
			require.NoError(t, err)

			config, err := LoadFromSources(tmpfile.Name(), nil)
			if tt.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}
			require.NoError(t, err)
			require.NotNil(t, config)
		})
	}
}

func TestParseMappingsEdgeCases(t *testing.T) {
	tests := []struct {
		name     string
		list     *MappingList
		wantErr  bool
		errCheck func(t *testing.T, err error)
	}{
		{
			name: "Empty mapping rule",
			list: &MappingList{
				ID:       "test",
				Mappings: []MappingRule{""},
			},
			wantErr: true,
			errCheck: func(t *testing.T, err error) {
				assert.Contains(t, err.Error(), "empty")
			},
		},
		{
			name: "Invalid mapping syntax",
			list: &MappingList{
				ID:       "test",
				Mappings: []MappingRule{"[A] -> [B]"},
			},
			wantErr: true,
			errCheck: func(t *testing.T, err error) {
				assert.Contains(t, err.Error(), "failed to parse")
			},
		},
		{
			name: "Missing brackets",
			list: &MappingList{
				ID:       "test",
				Mappings: []MappingRule{"A <> B"},
			},
			wantErr: true,
			errCheck: func(t *testing.T, err error) {
				assert.Contains(t, err.Error(), "failed to parse")
			},
		},
		{
			name: "Complex nested expressions",
			list: &MappingList{
				ID: "test",
				Mappings: []MappingRule{
					"[A & (B | C) & (D | (E & F))] <> [X & (Y | Z)]",
				},
			},
			wantErr: false,
		},
		{
			name: "Multiple foundry/layer combinations",
			list: &MappingList{
				ID: "test",
				Mappings: []MappingRule{
					"[foundry1/layer1=A & foundry2/layer2=B] <> [foundry3/layer3=C]",
				},
			},
			wantErr: false,
		},
		{
			name: "Default foundry/layer override",
			list: &MappingList{
				ID:       "test",
				FoundryA: "defaultFoundry",
				LayerA:   "defaultLayer",
				Mappings: []MappingRule{
					"[A] <> [B]", // Should use defaults
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results, err := tt.list.ParseMappings()
			if tt.wantErr {
				require.Error(t, err)
				if tt.errCheck != nil {
					tt.errCheck(t, err)
				}
				return
			}
			require.NoError(t, err)
			require.NotNil(t, results)
		})
	}
}

func TestUserProvidedMappingRules(t *testing.T) {
	// Test the exact YAML mapping rules provided by the user
	content := `
- id: stts-ud
  foundryA: opennlp
  layerA: p
  foundryB: upos
  layerB: p
  mappings:
    - "[$\\(] <> [PUNCT & PunctType=Brck]"
    - "[$,] <> [PUNCT & PunctType=Comm]"
    - "[$.] <> [PUNCT & PunctType=Peri]"
    - "[ADJA] <> [ADJ]"
    - "[ADJD] <> [ADJ & Variant=Short]"
    - "[ADV] <> [ADV]"
`
	tmpfile, err := os.CreateTemp("", "user-config-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	err = tmpfile.Close()
	require.NoError(t, err)

	// Test loading the configuration
	config, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)

	// Verify the configuration loaded correctly
	require.Len(t, config.Lists, 1)
	list := config.Lists[0]
	assert.Equal(t, "stts-ud", list.ID)
	assert.Equal(t, "opennlp", list.FoundryA)
	assert.Equal(t, "p", list.LayerA)
	assert.Equal(t, "upos", list.FoundryB)
	assert.Equal(t, "p", list.LayerB)
	require.Len(t, list.Mappings, 6)

	// First, test individual mappings to isolate the issue
	t.Run("parenthesis mapping", func(t *testing.T) {
		singleRule := &MappingList{
			ID:       "test-paren",
			FoundryA: "opennlp",
			LayerA:   "p",
			FoundryB: "upos",
			LayerB:   "p",
			Mappings: []MappingRule{"[$\\(] <> [PUNCT & PunctType=Brck]"},
		}
		results, err := singleRule.ParseMappings()
		require.NoError(t, err)
		require.Len(t, results, 1)

		upperTerm := results[0].Upper.Wrap.(*ast.Term)
		assert.Equal(t, "$(", upperTerm.Key)
	})

	t.Run("comma mapping", func(t *testing.T) {
		singleRule := &MappingList{
			ID:       "test-comma",
			FoundryA: "opennlp",
			LayerA:   "p",
			FoundryB: "upos",
			LayerB:   "p",
			Mappings: []MappingRule{"[$,] <> [PUNCT & PunctType=Comm]"},
		}
		results, err := singleRule.ParseMappings()
		require.NoError(t, err)
		require.Len(t, results, 1)

		upperTerm := results[0].Upper.Wrap.(*ast.Term)
		assert.Equal(t, "$,", upperTerm.Key)
	})

	t.Run("period mapping", func(t *testing.T) {
		singleRule := &MappingList{
			ID:       "test-period",
			FoundryA: "opennlp",
			LayerA:   "p",
			FoundryB: "upos",
			LayerB:   "p",
			Mappings: []MappingRule{"[$.] <> [PUNCT & PunctType=Peri]"},
		}
		results, err := singleRule.ParseMappings()
		require.NoError(t, err)
		require.Len(t, results, 1)

		upperTerm := results[0].Upper.Wrap.(*ast.Term)
		assert.Equal(t, "$.", upperTerm.Key)
	})

	// Test that all mapping rules can be parsed successfully
	results, err := list.ParseMappings()
	require.NoError(t, err)
	require.Len(t, results, 6)

	// Verify specific parsing of the special character mapping
	// The first mapping "[$\\(] <> [PUNCT & PunctType=Brck]" should parse correctly
	firstMapping := results[0]
	require.NotNil(t, firstMapping.Upper)
	upperTerm := firstMapping.Upper.Wrap.(*ast.Term)
	assert.Equal(t, "$(", upperTerm.Key) // The actual parsed key should be "$("
	assert.Equal(t, "opennlp", upperTerm.Foundry)
	assert.Equal(t, "p", upperTerm.Layer)

	require.NotNil(t, firstMapping.Lower)
	lowerGroup := firstMapping.Lower.Wrap.(*ast.TermGroup)
	require.Len(t, lowerGroup.Operands, 2)
	assert.Equal(t, ast.AndRelation, lowerGroup.Relation)

	// Check the PUNCT term
	punctTerm := lowerGroup.Operands[0].(*ast.Term)
	assert.Equal(t, "PUNCT", punctTerm.Key)
	assert.Equal(t, "upos", punctTerm.Foundry)
	assert.Equal(t, "p", punctTerm.Layer)

	// Check the PunctType term
	punctTypeTerm := lowerGroup.Operands[1].(*ast.Term)
	assert.Equal(t, "PunctType", punctTypeTerm.Layer)
	assert.Equal(t, "Brck", punctTypeTerm.Key)
	assert.Equal(t, "upos", punctTypeTerm.Foundry)

	// Verify the comma mapping as well
	secondMapping := results[1]
	upperTerm2 := secondMapping.Upper.Wrap.(*ast.Term)
	assert.Equal(t, "$,", upperTerm2.Key)

	// Verify the period mapping
	thirdMapping := results[2]
	upperTerm3 := thirdMapping.Upper.Wrap.(*ast.Term)
	assert.Equal(t, "$.", upperTerm3.Key)

	// Verify basic mappings without special characters
	fourthMapping := results[3]
	upperTerm4 := fourthMapping.Upper.Wrap.(*ast.Term)
	assert.Equal(t, "ADJA", upperTerm4.Key)
	lowerTerm4 := fourthMapping.Lower.Wrap.(*ast.Term)
	assert.Equal(t, "ADJ", lowerTerm4.Key)
}

func TestConfigWithSdkAndServer(t *testing.T) {
	tests := []struct {
		name           string
		content        string
		expectedSDK    string
		expectedServer string
		wantErr        bool
	}{
		{
			name: "Configuration with SDK and Server values",
			content: `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
lists:
- id: test-mapper
  foundryA: opennlp
  layerA: p
  foundryB: upos
  layerB: p
  mappings:
    - "[A] <> [B]"
`,
			expectedSDK:    "https://custom.example.com/sdk.js",
			expectedServer: "https://custom.example.com/",
			wantErr:        false,
		},
		{
			name: "Configuration with only SDK value",
			content: `
sdk: "https://custom.example.com/sdk.js"
lists:
- id: test-mapper
  mappings:
    - "[A] <> [B]"
`,
			expectedSDK:    "https://custom.example.com/sdk.js",
			expectedServer: "https://korap.ids-mannheim.de/", // default applied
			wantErr:        false,
		},
		{
			name: "Configuration with only Server value",
			content: `
server: "https://custom.example.com/"
lists:
- id: test-mapper
  mappings:
    - "[A] <> [B]"
`,
			expectedSDK:    "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
			expectedServer: "https://custom.example.com/",
			wantErr:        false,
		},
		{
			name: "Configuration without SDK and Server (old format with defaults applied)",
			content: `
- id: test-mapper
  mappings:
    - "[A] <> [B]"
`,
			expectedSDK:    "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
			expectedServer: "https://korap.ids-mannheim.de/",                          // default applied
			wantErr:        false,
		},
		{
			name: "Configuration with lists field explicitly",
			content: `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
lists:
- id: test-mapper-1
  mappings:
    - "[A] <> [B]"
- id: test-mapper-2
  mappings:
    - "[C] <> [D]"
`,
			expectedSDK:    "https://custom.example.com/sdk.js",
			expectedServer: "https://custom.example.com/",
			wantErr:        false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpfile, err := os.CreateTemp("", "config-*.yaml")
			require.NoError(t, err)
			defer os.Remove(tmpfile.Name())

			_, err = tmpfile.WriteString(tt.content)
			require.NoError(t, err)
			err = tmpfile.Close()
			require.NoError(t, err)

			config, err := LoadFromSources(tmpfile.Name(), nil)
			if tt.wantErr {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, config)

			// Check SDK and Server values
			assert.Equal(t, tt.expectedSDK, config.SDK)
			assert.Equal(t, tt.expectedServer, config.Server)

			// Ensure lists are still loaded correctly
			require.Greater(t, len(config.Lists), 0)

			// Verify first mapping list
			firstList := config.Lists[0]
			assert.NotEmpty(t, firstList.ID)
			assert.Greater(t, len(firstList.Mappings), 0)
		})
	}
}

func TestLoadFromSources(t *testing.T) {
	// Create main config file
	mainConfigContent := `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
lists:
- id: main-mapper
  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 files
	mappingFile1Content := `
id: mapper-1
foundryA: opennlp
layerA: p
mappings:
  - "[C] <> [D]"
`
	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: mapper-2
foundryB: upos
layerB: p
mappings:
  - "[E] <> [F]"
`
	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)

	tests := []struct {
		name         string
		configFile   string
		mappingFiles []string
		wantErr      bool
		expectedIDs  []string
	}{
		{
			name:         "Main config only",
			configFile:   mainConfigFile.Name(),
			mappingFiles: []string{},
			wantErr:      false,
			expectedIDs:  []string{"main-mapper"},
		},
		{
			name:         "Mapping files only",
			configFile:   "",
			mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
			wantErr:      false,
			expectedIDs:  []string{"mapper-1", "mapper-2"},
		},
		{
			name:         "Main config and mapping files",
			configFile:   mainConfigFile.Name(),
			mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
			wantErr:      false,
			expectedIDs:  []string{"main-mapper", "mapper-1", "mapper-2"},
		},
		{
			name:         "No configuration sources",
			configFile:   "",
			mappingFiles: []string{},
			wantErr:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config, err := LoadFromSources(tt.configFile, tt.mappingFiles)
			if tt.wantErr {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, config)

			// Check that all expected mapping IDs are present
			require.Len(t, config.Lists, len(tt.expectedIDs))
			actualIDs := make([]string, len(config.Lists))
			for i, list := range config.Lists {
				actualIDs[i] = list.ID
			}
			for _, expectedID := range tt.expectedIDs {
				assert.Contains(t, actualIDs, expectedID)
			}

			// Check that SDK and Server are set (either from config or defaults)
			assert.NotEmpty(t, config.SDK)
			assert.NotEmpty(t, config.Stylesheet)
			assert.NotEmpty(t, config.Server)
		})
	}
}

func TestLoadFromSourcesWithDefaults(t *testing.T) {
	// Test that defaults are applied when loading only mapping files
	mappingFileContent := `
id: test-mapper
mappings:
  - "[A] <> [B]"
`
	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)

	config, err := LoadFromSources("", []string{mappingFile.Name()})
	require.NoError(t, err)

	// Check that defaults are applied
	assert.Equal(t, defaultSDK, config.SDK)
	assert.Equal(t, defaultStylesheet, config.Stylesheet)
	assert.Equal(t, defaultServer, config.Server)
	require.Len(t, config.Lists, 1)
	assert.Equal(t, "test-mapper", config.Lists[0].ID)
}

func TestLoadFromSourcesDuplicateIDs(t *testing.T) {
	// Set up a buffer to capture log output
	var buf bytes.Buffer
	originalLogger := log.Logger
	defer func() {
		log.Logger = originalLogger
	}()
	log.Logger = log.Logger.Output(&buf)

	// Create config with duplicate IDs across sources
	configContent := `
lists:
- id: duplicate-id
  mappings:
    - "[A] <> [B]"
`
	configFile, err := os.CreateTemp("", "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)

	mappingContent := `
id: duplicate-id
mappings:
  - "[C] <> [D]"
`
	mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mappingFile.Name())

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

	// The function should now succeed but log the duplicate ID error
	config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
	require.NoError(t, err)
	require.NotNil(t, config)

	// Check that the duplicate ID error was logged
	logOutput := buf.String()
	assert.Contains(t, logOutput, "Duplicate mapping list ID found")
	assert.Contains(t, logOutput, "duplicate-id")

	// Only the first mapping list (from config file) should be loaded
	require.Len(t, config.Lists, 1)
	assert.Equal(t, "duplicate-id", config.Lists[0].ID)
	// Check that it's the one from the config file (has mapping "[A] <> [B]")
	assert.Equal(t, "[A] <> [B]", string(config.Lists[0].Mappings[0]))
}

func TestLoadFromSourcesConfigWithOnlyPort(t *testing.T) {
	// Create config file with only port (no lists)
	configContent := `
port: 8080
loglevel: debug
`
	configFile, err := os.CreateTemp("", "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)

	// Create mapping file
	mappingContent := `
id: test-mapper
mappings:
  - "[A] <> [B]"
`
	mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
	require.NoError(t, err)
	defer os.Remove(mappingFile.Name())

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

	// This should work: config file has only port, mapping file provides the mapping list
	config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
	require.NoError(t, err)
	require.NotNil(t, config)

	// Check that port and log level from config file are preserved
	assert.Equal(t, 8080, config.Port)
	assert.Equal(t, "debug", config.LogLevel)

	// Check that mapping from mapping file is loaded
	require.Len(t, config.Lists, 1)
	assert.Equal(t, "test-mapper", config.Lists[0].ID)

	// Check that defaults are applied for other fields
	assert.Equal(t, defaultSDK, config.SDK)
	assert.Equal(t, defaultStylesheet, config.Stylesheet)
	assert.Equal(t, defaultServer, config.Server)
	assert.Equal(t, defaultServiceURL, config.ServiceURL)
}

func TestCorpusMappingListType(t *testing.T) {
	content := `
lists:
- id: corpus-class-mapping
  type: corpus
  desc: Maps textClass values to genre field
  mappings:
    - "textClass=novel <> genre=fiction"
    - "textClass=science <> genre=nonfiction"
- id: annotation-mapper
  mappings:
    - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-corpus-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	err = tmpfile.Close()
	require.NoError(t, err)

	config, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	require.Len(t, config.Lists, 2)

	assert.Equal(t, "corpus", config.Lists[0].Type)
	assert.True(t, config.Lists[0].IsCorpus())

	assert.Equal(t, "", config.Lists[1].Type)
	assert.False(t, config.Lists[1].IsCorpus())
}

func TestParseCorpusMappings(t *testing.T) {
	list := &MappingList{
		ID:   "test-corpus",
		Type: "corpus",
		Mappings: []MappingRule{
			"textClass=novel <> genre=fiction",
			"(textClass=novel & pubDate=2020:geq#date) <> genre=recentfiction",
		},
	}

	results, err := list.ParseCorpusMappings()
	require.NoError(t, err)
	require.Len(t, results, 2)

	// Verify simple field rule
	require.NotNil(t, results[0].Upper)
	require.NotNil(t, results[0].Lower)

	// Verify group rule
	require.NotNil(t, results[1].Upper)
	require.NotNil(t, results[1].Lower)
}

func TestParseCorpusMappingsErrors(t *testing.T) {
	list := &MappingList{
		ID:       "test-corpus",
		Type:     "corpus",
		Mappings: []MappingRule{""},
	}

	_, err := list.ParseCorpusMappings()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "empty corpus mapping rule")

	list2 := &MappingList{
		ID:       "test-corpus",
		Type:     "corpus",
		Mappings: []MappingRule{"invalid rule without separator"},
	}

	_, err = list2.ParseCorpusMappings()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to parse corpus mapping rule")
}

func TestApplyEnvOverrides(t *testing.T) {
	envKeys := []string{
		"KORAL_MAPPER_SERVER",
		"KORAL_MAPPER_SDK",
		"KORAL_MAPPER_STYLESHEET",
		"KORAL_MAPPER_SERVICE_URL",
		"KORAL_MAPPER_COOKIE_NAME",
		"KORAL_MAPPER_PORT",
		"KORAL_MAPPER_LOG_LEVEL",
		"KORAL_MAPPER_ALLOW_ORIGINS",
	}

	clearEnv := func() {
		for _, key := range envKeys {
			os.Unsetenv(key)
		}
	}

	t.Run("ENV overrides config values", func(t *testing.T) {
		clearEnv()
		defer clearEnv()

		cfg := &MappingConfig{
			Server:     "from-config",
			SDK:        "from-config",
			Stylesheet: "from-config",
			ServiceURL: "from-config",
			CookieName: "from-config",
			Port:       1234,
			LogLevel:   "warn",
		}

		os.Setenv("KORAL_MAPPER_SERVER", "from-env-server")
		os.Setenv("KORAL_MAPPER_SDK", "from-env-sdk")
		os.Setenv("KORAL_MAPPER_STYLESHEET", "from-env-style")
		os.Setenv("KORAL_MAPPER_SERVICE_URL", "from-env-url")
		os.Setenv("KORAL_MAPPER_COOKIE_NAME", "from-env-cookie")
		os.Setenv("KORAL_MAPPER_PORT", "9999")
		os.Setenv("KORAL_MAPPER_LOG_LEVEL", "debug")

		ApplyEnvOverrides(cfg)

		assert.Equal(t, "from-env-server", cfg.Server)
		assert.Equal(t, "from-env-sdk", cfg.SDK)
		assert.Equal(t, "from-env-style", cfg.Stylesheet)
		assert.Equal(t, "from-env-url", cfg.ServiceURL)
		assert.Equal(t, "from-env-cookie", cfg.CookieName)
		assert.Equal(t, 9999, cfg.Port)
		assert.Equal(t, "debug", cfg.LogLevel)
	})

	t.Run("Empty ENV does not override", func(t *testing.T) {
		clearEnv()
		defer clearEnv()

		cfg := &MappingConfig{
			Server:     "original-server",
			SDK:        "original-sdk",
			Stylesheet: "original-style",
			ServiceURL: "original-url",
			CookieName: "original-cookie",
			Port:       1234,
			LogLevel:   "info",
		}

		ApplyEnvOverrides(cfg)

		assert.Equal(t, "original-server", cfg.Server)
		assert.Equal(t, "original-sdk", cfg.SDK)
		assert.Equal(t, "original-style", cfg.Stylesheet)
		assert.Equal(t, "original-url", cfg.ServiceURL)
		assert.Equal(t, "original-cookie", cfg.CookieName)
		assert.Equal(t, 1234, cfg.Port)
		assert.Equal(t, "info", cfg.LogLevel)
	})

	t.Run("Invalid port ENV is ignored", func(t *testing.T) {
		clearEnv()
		defer clearEnv()

		cfg := &MappingConfig{Port: 5725}
		os.Setenv("KORAL_MAPPER_PORT", "not-a-number")

		ApplyEnvOverrides(cfg)

		assert.Equal(t, 5725, cfg.Port)
	})

	t.Run("Partial ENV overrides", func(t *testing.T) {
		clearEnv()
		defer clearEnv()

		cfg := &MappingConfig{
			Server:   "from-config",
			SDK:      "from-config",
			Port:     1234,
			LogLevel: "warn",
		}

		os.Setenv("KORAL_MAPPER_SERVER", "from-env")
		os.Setenv("KORAL_MAPPER_PORT", "8080")

		ApplyEnvOverrides(cfg)

		assert.Equal(t, "from-env", cfg.Server)
		assert.Equal(t, "from-config", cfg.SDK)
		assert.Equal(t, 8080, cfg.Port)
		assert.Equal(t, "warn", cfg.LogLevel)
	})
}

func TestBasePathEnvOverride(t *testing.T) {
	t.Setenv("KORAL_MAPPER_BASE_PATH", "/custom/base/path")

	cfg := &MappingConfig{BasePath: "from-config"}
	ApplyEnvOverrides(cfg)

	assert.Equal(t, "/custom/base/path", cfg.BasePath)
}

func TestBasePathFromYAML(t *testing.T) {
	content := `
basePath: "/opt/koralmapper"
lists:
  - id: test-mapper
    mappings:
      - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-basepath-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	assert.Equal(t, "/opt/koralmapper", cfg.BasePath)
}

func TestEnvOverridesInLoadFromSources(t *testing.T) {
	envKeys := []string{
		"KORAL_MAPPER_SERVER",
		"KORAL_MAPPER_SDK",
		"KORAL_MAPPER_PORT",
		"KORAL_MAPPER_LOG_LEVEL",
		"KORAL_MAPPER_STYLESHEET",
		"KORAL_MAPPER_SERVICE_URL",
		"KORAL_MAPPER_COOKIE_NAME",
		"KORAL_MAPPER_ALLOW_ORIGINS",
	}
	clearEnv := func() {
		for _, key := range envKeys {
			os.Unsetenv(key)
		}
	}
	clearEnv()
	defer clearEnv()

	configContent := `
sdk: "https://custom.example.com/sdk.js"
server: "https://custom.example.com/"
port: 3000
lists:
- id: test-mapper
  mappings:
    - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-env-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

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

	os.Setenv("KORAL_MAPPER_SERVER", "https://env-override.example.com/")
	os.Setenv("KORAL_MAPPER_PORT", "7777")

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)

	// ENV overrides YAML values
	assert.Equal(t, "https://env-override.example.com/", cfg.Server)
	assert.Equal(t, 7777, cfg.Port)

	// Non-overridden values preserved from YAML
	assert.Equal(t, "https://custom.example.com/sdk.js", cfg.SDK)

	// Defaults applied for unset fields
	assert.Equal(t, defaultStylesheet, cfg.Stylesheet)
	assert.Equal(t, defaultServiceURL, cfg.ServiceURL)
	assert.Equal(t, defaultCookieName, cfg.CookieName)
	assert.Equal(t, defaultLogLevel, cfg.LogLevel)
}

func TestRewritesYAMLField(t *testing.T) {
	content := `
lists:
  - id: rewrite-on
    rewrites: true
    mappings:
      - "[A] <> [B]"
  - id: rewrite-off
    rewrites: false
    mappings:
      - "[C] <> [D]"
  - id: rewrite-default
    mappings:
      - "[E] <> [F]"
`
	tmpfile, err := os.CreateTemp("", "config-rewrites-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	require.Len(t, cfg.Lists, 3)

	assert.True(t, cfg.Lists[0].Rewrites, "rewrites should be true when set to true")
	assert.False(t, cfg.Lists[1].Rewrites, "rewrites should be false when set to false")
	assert.False(t, cfg.Lists[2].Rewrites, "rewrites should default to false")
}

func TestParseCorpusMappingsWithFieldAFieldB(t *testing.T) {
	list := &MappingList{
		ID:     "test-keyed",
		Type:   "corpus",
		FieldA: "wikiCat",
		FieldB: "textClass",
		Mappings: []MappingRule{
			"Entertainment <> ((kultur & musik) | (kultur & film))",
		},
	}

	results, err := list.ParseCorpusMappings()
	require.NoError(t, err)
	require.Len(t, results, 1)

	upper := results[0].Upper.(*parser.CorpusField)
	assert.Equal(t, "wikiCat", upper.Key)
	assert.Equal(t, "Entertainment", upper.Value)

	group := results[0].Lower.(*parser.CorpusGroup)
	assert.Equal(t, "or", group.Operation)
	require.Len(t, group.Operands, 2)

	and1 := group.Operands[0].(*parser.CorpusGroup)
	assert.Equal(t, "textClass", and1.Operands[0].(*parser.CorpusField).Key)
	assert.Equal(t, "kultur", and1.Operands[0].(*parser.CorpusField).Value)
	assert.Equal(t, "textClass", and1.Operands[1].(*parser.CorpusField).Key)
	assert.Equal(t, "musik", and1.Operands[1].(*parser.CorpusField).Value)
}

func TestRateLimitConfigField(t *testing.T) {
	content := `
rateLimit: 50
lists:
  - id: test-mapper
    mappings:
      - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-ratelimit-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	assert.Equal(t, 50, cfg.RateLimit, "rateLimit should be loaded from YAML")
}

func TestRateLimitDefaultApplied(t *testing.T) {
	cfg := &MappingConfig{}
	ApplyDefaults(cfg)
	assert.Equal(t, defaultRateLimit, cfg.RateLimit,
		"default rate limit should be applied when not specified")
}

func TestRateLimitEnvOverride(t *testing.T) {
	t.Setenv("KORAL_MAPPER_RATE_LIMIT", "200")

	content := `
rateLimit: 50
lists:
  - id: test-mapper
    mappings:
      - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-ratelimit-env-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	assert.Equal(t, 200, cfg.RateLimit,
		"KORAL_MAPPER_RATE_LIMIT env var should override YAML value")
}

func TestAllowOriginsDefault(t *testing.T) {
	cfg := &MappingConfig{}
	ApplyDefaults(cfg)
	// AllowOrigins should derive from the Server default (trailing slash stripped)
	assert.Equal(t, "https://korap.ids-mannheim.de", cfg.AllowOrigins,
		"default AllowOrigins should derive from defaultServer")
}

func TestAllowOriginsDerivedFromCustomServer(t *testing.T) {
	cfg := &MappingConfig{
		Server: "https://custom.example.com/",
	}
	ApplyDefaults(cfg)
	assert.Equal(t, "https://custom.example.com", cfg.AllowOrigins,
		"AllowOrigins should derive from the configured Server (trailing slash stripped)")
}

func TestAllowOriginsExplicitNotOverriddenByServer(t *testing.T) {
	cfg := &MappingConfig{
		Server:       "https://custom.example.com/",
		AllowOrigins: "https://explicit-origin.example.com",
	}
	ApplyDefaults(cfg)
	assert.Equal(t, "https://explicit-origin.example.com", cfg.AllowOrigins,
		"explicit AllowOrigins should not be overridden by Server default")
}

func TestAllowOriginsFromYAML(t *testing.T) {
	content := `
allowOrigins: "https://custom.example.com,https://other.example.com"
lists:
  - id: test-mapper
    mappings:
      - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-cors-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	assert.Equal(t, "https://custom.example.com,https://other.example.com",
		cfg.AllowOrigins)
}

func TestAllowOriginsEnvOverride(t *testing.T) {
	t.Setenv("KORAL_MAPPER_ALLOW_ORIGINS", "https://env-origin.example.com")

	content := `
allowOrigins: "https://yaml-origin.example.com"
lists:
  - id: test-mapper
    mappings:
      - "[A] <> [B]"
`
	tmpfile, err := os.CreateTemp("", "config-cors-env-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources(tmpfile.Name(), nil)
	require.NoError(t, err)
	assert.Equal(t, "https://env-origin.example.com", cfg.AllowOrigins,
		"KORAL_MAPPER_ALLOW_ORIGINS env var should override YAML value")
}

func TestSanitizeFilePathRejectsOutsideBase(t *testing.T) {
	// Set base to a specific directory and verify paths outside are rejected
	tmpDir, err := os.MkdirTemp("", "koral-base-*")
	require.NoError(t, err)
	defer os.RemoveAll(tmpDir)

	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = tmpDir

	tests := []struct {
		name    string
		input   string
		wantErr bool
	}{
		{
			name:    "Path within base is accepted",
			input:   filepath.Join(tmpDir, "config.yaml"),
			wantErr: false,
		},
		{
			name:    "Path outside base is rejected",
			input:   "/etc/passwd",
			wantErr: true,
		},
		{
			name:    "Traversal escaping base and tmp is rejected",
			input:   "/etc/passwd",
			wantErr: true,
		},
		{
			name:    "Empty path is rejected",
			input:   "",
			wantErr: true,
		},
		{
			name:    "Subdirectory within base is accepted",
			input:   filepath.Join(tmpDir, "sub", "dir", "file.yaml"),
			wantErr: false,
		},
		{
			name:    "Relative path within base is rejected when CWD differs",
			input:   "config.yaml",
			wantErr: true, // resolves against CWD, not base
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := sanitizeFilePath(tt.input)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			assert.True(t, filepath.IsAbs(result),
				"sanitized path should be absolute, got: %s", result)
			assert.NotContains(t, result, "..")
		})
	}
}

func TestSanitizeFilePathTraversalToPasswd(t *testing.T) {
	// Verify /etc/passwd cannot be accessed via traversal
	cwd, err := os.Getwd()
	require.NoError(t, err)

	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = cwd

	_, err = sanitizeFilePath("../../../etc/passwd")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "path traversal detected")
}

func TestSanitizeFilePathWithDockerRoot(t *testing.T) {
	// In Docker the WORKDIR is "/" -- all absolute paths should be valid
	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = "/"

	result, err := sanitizeFilePath("/mappings/stts-upos.yaml")
	require.NoError(t, err)
	assert.Equal(t, "/mappings/stts-upos.yaml", result)

	// Even deeply nested paths work when base is /
	result, err = sanitizeFilePath("/etc/ssl/certs/ca-certificates.crt")
	require.NoError(t, err)
	assert.Equal(t, "/etc/ssl/certs/ca-certificates.crt", result)
}

func TestSanitizeFilePathPrefixFalsePositive(t *testing.T) {
	// Ensure /home/user does not match /home/username
	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = "/home/user"

	_, err := sanitizeFilePath("/home/username/secret.yaml")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "path traversal detected")
}

func TestLoadFromSourcesRejectsTraversal(t *testing.T) {
	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()

	cwd, err := os.Getwd()
	require.NoError(t, err)
	AllowedBasePath = cwd

	// Config file traversal should be rejected
	_, err = LoadFromSources("../../../etc/passwd", nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "path traversal detected")

	// Mapping file traversal should be rejected
	_, err = LoadFromSources("", []string{"../../../etc/passwd"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "path traversal detected")
}

func TestValidPathsStillWork(t *testing.T) {
	content := `
id: test-mapper
mappings:
  - "[A] <> [B]"
`
	tmpDir, err := os.MkdirTemp("", "koral-test-*")
	require.NoError(t, err)
	defer os.RemoveAll(tmpDir)

	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = tmpDir

	subDir := filepath.Join(tmpDir, "subdir")
	require.NoError(t, os.Mkdir(subDir, 0755))

	tmpfile, err := os.CreateTemp(subDir, "mapping-*.yaml")
	require.NoError(t, err)

	_, err = tmpfile.WriteString(content)
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	cfg, err := LoadFromSources("", []string{tmpfile.Name()})
	require.NoError(t, err)
	require.Len(t, cfg.Lists, 1)
	assert.Equal(t, "test-mapper", cfg.Lists[0].ID)
}

func TestRelativePathWithTraversalWithinBase(t *testing.T) {
	// Paths with ".." that still resolve within the base should work
	content := `
id: traversal-test-mapper
mappings:
  - "[A] <> [B]"
`
	tmpDir, err := os.MkdirTemp("", "koral-traversal-*")
	require.NoError(t, err)
	defer os.RemoveAll(tmpDir)

	origBase := AllowedBasePath
	defer func() { AllowedBasePath = origBase }()
	AllowedBasePath = tmpDir

	// Create file at tmpDir/config.yaml
	configPath := filepath.Join(tmpDir, "config.yaml")
	require.NoError(t, os.WriteFile(configPath, []byte(content), 0644))

	// Reference via a traversal path: tmpDir/subdir/../config.yaml
	// This resolves to tmpDir/config.yaml which is within the base
	subDir := filepath.Join(tmpDir, "subdir")
	require.NoError(t, os.Mkdir(subDir, 0755))
	traversalPath := filepath.Join(subDir, "..", "config.yaml")

	cfg, err := LoadFromSources("", []string{traversalPath})
	require.NoError(t, err)
	require.Len(t, cfg.Lists, 1)
	assert.Equal(t, "traversal-test-mapper", cfg.Lists[0].ID)
}
