// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package config

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"go.uber.org/mock/gomock"

	"github.com/stacklok/toolhive-core/env"
	"github.com/stacklok/toolhive-core/env/mocks"
	"github.com/stacklok/toolhive/pkg/vmcp"
)

// createMockEnvReader creates a mock env.Reader with expectations based on the envVars map.
func createMockEnvReader(t *testing.T, envVars map[string]string) *mocks.MockReader {
	t.Helper()
	ctrl := gomock.NewController(t)
	mockEnv := mocks.NewMockReader(ctrl)

	// Set up expectations for each env var
	for key, value := range envVars {
		mockEnv.EXPECT().Getenv(key).Return(value).AnyTimes()
	}

	// For any other keys, return empty string
	mockEnv.EXPECT().Getenv(gomock.Any()).Return("").AnyTimes()

	return mockEnv
}

func TestYAMLLoader_Load(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name    string
		yaml    string
		envVars map[string]string
		want    func(*testing.T, *Config)
		wantErr bool
		errMsg  string
	}{
		{
			name: "valid minimal configuration",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.Name != "test-vmcp" {
					t.Errorf("Name = %v, want test-vmcp", cfg.Name)
				}
				if cfg.Group != "test-group" {
					t.Errorf("Group = %v, want test-group", cfg.Group)
				}
				if cfg.IncomingAuth.Type != "anonymous" {
					t.Errorf("IncomingAuth.Type = %v, want anonymous", cfg.IncomingAuth.Type)
				}
				if cfg.OutgoingAuth.Source != "inline" {
					t.Errorf("OutgoingAuth.Source = %v, want inline", cfg.OutgoingAuth.Source)
				}
				if cfg.Aggregation.ConflictResolution != vmcp.ConflictStrategyPrefix {
					t.Errorf("ConflictResolution = %v, want prefix", cfg.Aggregation.ConflictResolution)
				}
			},
			wantErr: false,
		},
		{
			name: "valid OIDC configuration with env vars",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: oidc
  oidc:
    issuer: https://auth.example.com
    clientId: test-client
    clientSecretEnv: TEST_SECRET
    audience: vmcp
    scopes:
      - openid
      - profile

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			envVars: map[string]string{
				"TEST_SECRET": "my-secret-value",
			},
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.IncomingAuth.Type != "oidc" {
					t.Errorf("IncomingAuth.Type = %v, want oidc", cfg.IncomingAuth.Type)
				}
				if cfg.IncomingAuth.OIDC == nil {
					t.Fatal("IncomingAuth.OIDC is nil")
				}
				if cfg.IncomingAuth.OIDC.Issuer != "https://auth.example.com" {
					t.Errorf("OIDC.Issuer = %v, want https://auth.example.com", cfg.IncomingAuth.OIDC.Issuer)
				}
				if cfg.IncomingAuth.OIDC.ClientSecretEnv != "TEST_SECRET" {
					t.Errorf("OIDC.ClientSecretEnv = %v, want TEST_SECRET", cfg.IncomingAuth.OIDC.ClientSecretEnv)
				}
			},
			wantErr: false,
		},
		{
			name: "valid OIDC configuration with jwksUrl and introspectionUrl",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: oidc
  oidc:
    issuer: https://auth.example.com
    clientId: test-client
    audience: vmcp
    jwksUrl: https://auth.example.com/custom/jwks
    introspectionUrl: https://auth.example.com/custom/introspect

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.IncomingAuth.OIDC == nil {
					t.Fatal("IncomingAuth.OIDC is nil")
				}
				if cfg.IncomingAuth.OIDC.JWKSURL != "https://auth.example.com/custom/jwks" {
					t.Errorf("OIDC.JWKSURL = %v, want https://auth.example.com/custom/jwks", cfg.IncomingAuth.OIDC.JWKSURL)
				}
				if cfg.IncomingAuth.OIDC.IntrospectionURL != "https://auth.example.com/custom/introspect" {
					t.Errorf("OIDC.IntrospectionURL = %v, want https://auth.example.com/custom/introspect", cfg.IncomingAuth.OIDC.IntrospectionURL)
				}
			},
			wantErr: false,
		},
		{
			name: "partial operational config gets defaults for missing fields",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"

operational:
  timeouts:
    default: 45s
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.Operational == nil {
					t.Fatal("Operational should not be nil")
				}
				// Custom timeout should be preserved
				if cfg.Operational.Timeouts.Default != Duration(45*time.Second) {
					t.Errorf("Timeouts.Default = %v, want 45s", cfg.Operational.Timeouts.Default)
				}
				// FailureHandling should be created with defaults
				if cfg.Operational.FailureHandling == nil {
					t.Fatal("FailureHandling should not be nil")
				}
				if cfg.Operational.FailureHandling.HealthCheckInterval != Duration(30*time.Second) {
					t.Errorf("HealthCheckInterval = %v, want 30s (default)", cfg.Operational.FailureHandling.HealthCheckInterval)
				}
				if cfg.Operational.FailureHandling.UnhealthyThreshold != 3 {
					t.Errorf("UnhealthyThreshold = %v, want 3 (default)", cfg.Operational.FailureHandling.UnhealthyThreshold)
				}
				if cfg.Operational.FailureHandling.CircuitBreaker == nil {
					t.Fatal("CircuitBreaker should not be nil (should get defaults)")
				}
			},
			wantErr: false,
		},
		{
			name: "valid configuration with composite tools",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"

compositeTools:
  - name: deploy_workflow
    description: Deploy and notify
    parameters:
      type: object
      properties:
        pr_number:
          type: integer
    timeout: 30m
    steps:
      - id: merge
        type: tool
        tool: github.merge_pr
        arguments:
          pr: "{{.params.pr_number}}"
      - id: notify
        type: tool
        tool: slack.post_message
        arguments:
          message: "Deployed PR {{.params.pr_number}}"
        dependsOn:
          - merge
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if len(cfg.CompositeTools) != 1 {
					t.Fatalf("CompositeTools length = %v, want 1", len(cfg.CompositeTools))
				}
				tool := cfg.CompositeTools[0]
				if tool.Name != "deploy_workflow" {
					t.Errorf("Tool.Name = %v, want deploy_workflow", tool.Name)
				}
				if tool.Timeout != Duration(30*time.Minute) {
					t.Errorf("Tool.Timeout = %v, want 30m", tool.Timeout)
				}
				if len(tool.Steps) != 2 {
					t.Errorf("Tool.Steps length = %v, want 2", len(tool.Steps))
				}
			},
			wantErr: false,
		},
		{
			name: "invalid YAML syntax",
			yaml: `
name: test-vmcp
groupRef: test-group
incoming_auth
  type: anonymous
`,
			wantErr: true,
			errMsg:  "failed to parse YAML",
		},
		{
			name: "OIDC with unset environment variable is allowed (validation happens at runtime)",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: oidc
  oidc:
    issuer: https://auth.example.com
    clientId: test-client
    clientSecretEnv: MISSING_VAR
    audience: vmcp

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.IncomingAuth.OIDC == nil {
					t.Fatal("IncomingAuth.OIDC is nil")
				}
				// Verify the env var name is stored (not resolved)
				if cfg.IncomingAuth.OIDC.ClientSecretEnv != "MISSING_VAR" {
					t.Errorf("OIDC.ClientSecretEnv = %v, want MISSING_VAR", cfg.IncomingAuth.OIDC.ClientSecretEnv)
				}
			},
			wantErr: false,
		},
		{
			name: "composite tool with missing parameter type",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"

compositeTools:
  - name: test_tool
    description: Test tool
    timeout: 5m
    parameters:
      properties:
        param1:
          type: string
          default: "value"
    steps:
      - id: step1
        type: tool
        tool: some.tool
`,
			wantErr: true,
			errMsg:  "parameters must have 'type' field",
		},
		{
			name: "header_injection with header_value_env resolves environment variable",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    github:
      type: header_injection
      headerInjection:
        headerName: "Authorization"
        headerValueEnv: "GITHUB_TOKEN"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			envVars: map[string]string{
				"GITHUB_TOKEN": "secret-token-123",
			},
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				backend, ok := cfg.OutgoingAuth.Backends["github"]
				if !ok {
					t.Fatal("github backend not found")
				}
				if backend.Type != "header_injection" {
					t.Errorf("Backend.Type = %v, want headerInjection", backend.Type)
				}
				// Verify the resolved value is in HeaderInjection config
				if backend.HeaderInjection == nil {
					t.Fatal("HeaderInjection is nil")
				}
				if backend.HeaderInjection.HeaderValue != "secret-token-123" {
					t.Errorf("HeaderInjection.HeaderValue = %v, want secret-token-123", backend.HeaderInjection.HeaderValue)
				}
			},
			wantErr: false,
		},
		{
			name: "header_injection with literal header_value works",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    api-service:
      type: header_injection
      headerInjection:
        headerName: "X-API-Version"
        headerValue: "v1"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				backend, ok := cfg.OutgoingAuth.Backends["api-service"]
				if !ok {
					t.Fatal("api-service backend not found")
				}
				if backend.HeaderInjection == nil {
					t.Fatal("HeaderInjection is nil")
				}
				if backend.HeaderInjection.HeaderValue != "v1" {
					t.Errorf("HeaderInjection.HeaderValue = %v, want v1", backend.HeaderInjection.HeaderValue)
				}
			},
			wantErr: false,
		},
		{
			name: "header_injection fails when env var not set",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    github:
      type: header_injection
      headerInjection:
        headerName: "Authorization"
        headerValueEnv: "MISSING_TOKEN"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			wantErr: true,
			errMsg:  "environment variable MISSING_TOKEN not set",
		},
		{
			name: "header_injection fails when both header_value and header_value_env set",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    github:
      type: header_injection
      headerInjection:
        headerName: "Authorization"
        headerValue: "literal-value"
        headerValueEnv: "ENV_VALUE"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			wantErr: true,
			errMsg:  "only one of headerValue or headerValueEnv must be set",
		},
		{
			name: "header_injection fails when neither header_value nor header_value_env set",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    github:
      type: header_injection
      headerInjection:
        headerName: "Authorization"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			wantErr: true,
			errMsg:  "either headerValue or headerValueEnv must be set",
		},
		{
			name: "header_injection fails when env var is empty string",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  backends:
    github:
      type: header_injection
      headerInjection:
        headerName: "Authorization"
        headerValueEnv: "EMPTY_TOKEN"

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			envVars: map[string]string{
				"EMPTY_TOKEN": "",
			},
			wantErr: true,
			errMsg:  "environment variable EMPTY_TOKEN not set or empty",
		},
		{
			name: "valid audit configuration",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"

audit:
  component: "vmcp-server"
  eventTypes:
    - "mcp_initialize"
    - "mcp_tool_call"
  excludeEventTypes:
    - "mcp_ping"
  includeRequestData: true
  includeResponseData: false
  maxDataSize: 10000
  logFile: "/var/log/vmcp/audit.log"
`,
			want: func(t *testing.T, cfg *Config) {
				t.Helper()
				if cfg.Audit == nil {
					t.Fatal("Audit should not be nil")
				}
				if cfg.Audit.Component != "vmcp-server" {
					t.Errorf("Audit.Component = %v, want vmcp-server", cfg.Audit.Component)
				}
				if len(cfg.Audit.EventTypes) != 2 || cfg.Audit.EventTypes[0] != "mcp_initialize" || cfg.Audit.EventTypes[1] != "mcp_tool_call" {
					t.Errorf("Audit.EventTypes = %v, want [mcp_initialize mcp_tool_call]", cfg.Audit.EventTypes)
				}
				if len(cfg.Audit.ExcludeEventTypes) != 1 || cfg.Audit.ExcludeEventTypes[0] != "mcp_ping" {
					t.Errorf("Audit.ExcludeEventTypes = %v, want [mcp_ping]", cfg.Audit.ExcludeEventTypes)
				}
				if !cfg.Audit.IncludeRequestData {
					t.Error("Audit.IncludeRequestData = false, want true")
				}
				if cfg.Audit.IncludeResponseData {
					t.Error("Audit.IncludeResponseData = true, want false")
				}
				if cfg.Audit.MaxDataSize != 10000 {
					t.Errorf("Audit.MaxDataSize = %v, want 10000", cfg.Audit.MaxDataSize)
				}
				if cfg.Audit.LogFile != "/var/log/vmcp/audit.log" {
					t.Errorf("Audit.LogFile = %v, want /var/log/vmcp/audit.log", cfg.Audit.LogFile)
				}
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Create mock env reader with test-specific env vars
			mockEnv := createMockEnvReader(t, tt.envVars)

			// Create temporary file with YAML content
			tmpDir := t.TempDir()
			tmpFile := filepath.Join(tmpDir, "config.yaml")
			if err := os.WriteFile(tmpFile, []byte(tt.yaml), 0644); err != nil {
				t.Fatalf("Failed to write temp file: %v", err)
			}

			// Load configuration
			loader := NewYAMLLoader(tmpFile, mockEnv)
			cfg, err := loader.Load()

			// Check error expectation
			if (err != nil) != tt.wantErr {
				t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if tt.wantErr && err != nil && tt.errMsg != "" {
				if !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf("Load() error message = %v, want to contain %v", err.Error(), tt.errMsg)
				}
				return
			}

			// Verify configuration
			if tt.want != nil && cfg != nil {
				tt.want(t, cfg)
			}
		})
	}
}

func TestYAMLLoader_LoadFileNotFound(t *testing.T) {
	t.Parallel()
	envReader := &env.OSReader{}
	loader := NewYAMLLoader("/non/existent/file.yaml", envReader)
	_, err := loader.Load()

	if err == nil {
		t.Error("Load() expected error for non-existent file, got nil")
	}

	if !strings.Contains(err.Error(), "failed to read config file") {
		t.Errorf("Load() error = %v, want to contain 'failed to read config file'", err)
	}
}

func TestYAMLLoader_IntegrationWithValidator(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name       string
		yaml       string
		envVars    map[string]string
		shouldPass bool
		errMsg     string
	}{
		{
			name: "valid configuration passes validation",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			shouldPass: true,
		},
		{
			name: "configuration with missing name fails validation",
			yaml: `
groupRef: test-group

incomingAuth:
  type: anonymous

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			shouldPass: false,
			errMsg:     "name is required",
		},
		{
			name: "configuration with invalid auth type fails validation",
			yaml: `
name: test-vmcp
groupRef: test-group

incomingAuth:
  type: invalid_type

outgoingAuth:
  source: inline
  default:
    type: unauthenticated

aggregation:
  conflictResolution: prefix
  conflictResolutionConfig:
    prefixFormat: "{workload}_"
`,
			shouldPass: false,
			errMsg:     "incomingAuth.type must be one of",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Create mock env reader with test-specific env vars
			mockEnv := createMockEnvReader(t, tt.envVars)

			// Create temporary file
			tmpDir := t.TempDir()
			tmpFile := filepath.Join(tmpDir, "config.yaml")
			if err := os.WriteFile(tmpFile, []byte(tt.yaml), 0644); err != nil {
				t.Fatalf("Failed to write temp file: %v", err)
			}

			// Load and validate
			loader := NewYAMLLoader(tmpFile, mockEnv)
			cfg, err := loader.Load()
			if err != nil {
				if tt.shouldPass {
					t.Fatalf("Load() unexpected error = %v", err)
				}
				return
			}

			validator := NewValidator()
			err = validator.Validate(cfg)

			if tt.shouldPass && err != nil {
				t.Errorf("Validate() unexpected error = %v", err)
			}

			if !tt.shouldPass && err == nil {
				t.Error("Validate() expected error, got nil")
			}

			if !tt.shouldPass && err != nil && tt.errMsg != "" {
				if !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf("Validate() error = %v, want to contain %v", err.Error(), tt.errMsg)
				}
			}
		})
	}
}
