package analyzer

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/pkg/errors"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewAnalysisEngine(t *testing.T) {
	engine := NewAnalysisEngine()

	assert.NotNil(t, engine)
	assert.Len(t, engine.ListAgents(), 0) // No agents registered initially
}

func TestAnalysisEngine_RegisterAgent(t *testing.T) {
	engine := NewAnalysisEngine()

	tests := []struct {
		name      string
		agentName string
		agent     Agent
		wantErr   bool
		errMsg    string
	}{
		{
			name:      "valid agent registration",
			agentName: "test-agent",
			agent:     &mockAgent{name: "test-agent"},
			wantErr:   false,
		},
		{
			name:      "empty agent name",
			agentName: "",
			agent:     &mockAgent{name: "test-agent"},
			wantErr:   true,
			errMsg:    "agent name cannot be empty",
		},
		{
			name:      "nil agent",
			agentName: "test-agent",
			agent:     nil,
			wantErr:   true,
			errMsg:    "agent cannot be nil",
		},
		{
			name:      "duplicate agent registration",
			agentName: "duplicate-agent",
			agent:     &mockAgent{name: "duplicate-agent"},
			wantErr:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := engine.RegisterAgent(tt.agentName, tt.agent)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
			} else {
				assert.NoError(t, err)

				// Verify agent was registered
				agent, exists := engine.GetAgent(tt.agentName)
				assert.True(t, exists)
				assert.Equal(t, tt.agent, agent)
			}
		})
	}

	// Test duplicate registration error with fresh engine
	freshEngine := NewAnalysisEngine()
	agent := &mockAgent{name: "duplicate-agent"}
	err := freshEngine.RegisterAgent("duplicate-agent", agent)
	require.NoError(t, err)

	err = freshEngine.RegisterAgent("duplicate-agent", agent)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already registered")
}

func TestAnalysisEngine_Analyze(t *testing.T) {
	engine := NewAnalysisEngine()

	// Register a mock agent
	mockAgent := &mockAgent{
		name:      "test-agent",
		available: true,
		results: []*AnalyzerResult{
			{
				Title:   "Test Result",
				Message: "Test message",
				IsPass:  true,
			},
		},
	}

	err := engine.RegisterAgent("test-agent", mockAgent)
	require.NoError(t, err)

	tests := []struct {
		name    string
		bundle  *SupportBundle
		opts    AnalysisOptions
		wantErr bool
		errMsg  string
	}{
		{
			name: "successful analysis",
			bundle: &SupportBundle{
				Files: map[string][]byte{
					"test.json": []byte(`{"test": "data"}`),
				},
				Metadata: &SupportBundleMetadata{
					CreatedAt: time.Now(),
					Version:   "1.0.0",
				},
			},
			opts: AnalysisOptions{
				Agents: []string{"test-agent"},
			},
			wantErr: false,
		},
		{
			name:    "nil bundle",
			bundle:  nil,
			opts:    AnalysisOptions{},
			wantErr: true,
			errMsg:  "bundle cannot be nil",
		},
		{
			name: "non-existent agent",
			bundle: &SupportBundle{
				Files: map[string][]byte{},
			},
			opts: AnalysisOptions{
				Agents: []string{"non-existent-agent"},
			},
			wantErr: true,
			errMsg:  "not registered",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			result, err := engine.Analyze(ctx, tt.bundle, tt.opts)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
				assert.Nil(t, result)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, result)

				// Verify basic result structure
				assert.NotNil(t, result.Results)
				assert.NotNil(t, result.Summary)
				assert.NotNil(t, result.Metadata)

				// Verify agent was used
				assert.Contains(t, result.Summary.AgentsUsed, "test-agent")
			}
		})
	}
}

func TestAnalysisEngine_GenerateAnalyzers(t *testing.T) {
	engine := NewAnalysisEngine()
	ctx := context.Background()

	tests := []struct {
		name         string
		requirements *RequirementSpec
		wantErr      bool
		errMsg       string
		wantSpecs    int
	}{
		{
			name:         "nil requirements",
			requirements: nil,
			wantErr:      true,
			errMsg:       "requirements cannot be nil",
		},
		{
			name: "kubernetes version requirements",
			requirements: &RequirementSpec{
				APIVersion: "troubleshoot.replicated.com/v1beta2",
				Kind:       "Requirements",
				Metadata: RequirementMetadata{
					Name: "test-requirements",
				},
				Spec: RequirementSpecDetails{
					Kubernetes: KubernetesRequirements{
						MinVersion: "1.20.0",
						MaxVersion: "1.25.0",
					},
				},
			},
			wantErr:   false,
			wantSpecs: 1,
		},
		{
			name: "resource requirements",
			requirements: &RequirementSpec{
				APIVersion: "troubleshoot.replicated.com/v1beta2",
				Kind:       "Requirements",
				Metadata: RequirementMetadata{
					Name: "resource-requirements",
				},
				Spec: RequirementSpecDetails{
					Resources: ResourceRequirements{
						CPU: ResourceRequirement{
							Min: "2",
						},
						Memory: ResourceRequirement{
							Min: "4Gi",
						},
					},
				},
			},
			wantErr:   false,
			wantSpecs: 1, // simplified engine implementation generates 1 analyzer
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			specs, err := engine.GenerateAnalyzers(ctx, tt.requirements)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
				assert.Nil(t, specs)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, specs)
				assert.Len(t, specs, tt.wantSpecs)

				// Verify all specs have required fields
				for i, spec := range specs {
					assert.NotEmpty(t, spec.Name, "spec %d should have name", i)
					assert.NotEmpty(t, spec.Type, "spec %d should have type", i)
					assert.NotEmpty(t, spec.Category, "spec %d should have category", i)
					assert.Greater(t, spec.Priority, 0, "spec %d should have positive priority", i)
				}
			}
		})
	}
}

func TestAnalysisEngine_HealthCheck(t *testing.T) {
	engine := NewAnalysisEngine()
	ctx := context.Background()

	// Test with no agents
	health, err := engine.HealthCheck(ctx)
	require.NoError(t, err)
	assert.Equal(t, "healthy", health.Status)
	assert.Empty(t, health.Agents)

	// Add healthy agent
	healthyAgent := &mockAgent{
		name:      "healthy-agent",
		available: true,
		healthy:   true,
	}
	err = engine.RegisterAgent("healthy-agent", healthyAgent)
	require.NoError(t, err)

	// Add unhealthy agent
	unhealthyAgent := &mockAgent{
		name:      "unhealthy-agent",
		available: false,
		healthy:   false,
		error:     "mock error",
	}
	err = engine.RegisterAgent("unhealthy-agent", unhealthyAgent)
	require.NoError(t, err)

	// Test health check with mixed agents
	health, err = engine.HealthCheck(ctx)
	require.NoError(t, err)
	assert.Equal(t, "degraded", health.Status)
	assert.Len(t, health.Agents, 2)

	// Find the unhealthy agent in results
	var unhealthyFound bool
	for _, agentHealth := range health.Agents {
		if agentHealth.Name == "unhealthy-agent" {
			assert.Equal(t, "unhealthy", agentHealth.Status)
			assert.Equal(t, "mock error", agentHealth.Error)
			assert.False(t, agentHealth.Available)
			unhealthyFound = true
		}
	}
	assert.True(t, unhealthyFound, "unhealthy agent should be found in health results")
}

func TestAnalysisEngine_calculateSummary(t *testing.T) {
	engine := &DefaultAnalysisEngine{}

	results := &AnalysisResult{
		Results: []*AnalyzerResult{
			{IsPass: true, Confidence: 0.9},
			{IsWarn: true, Confidence: 0.8},
			{IsFail: true, Confidence: 0.7},
			{IsPass: true, Confidence: 0.0}, // No confidence
		},
		Errors: []AnalysisError{
			{Error: "test error"},
		},
	}

	engine.calculateSummary(results)

	assert.Equal(t, 4, results.Summary.TotalAnalyzers)
	assert.Equal(t, 2, results.Summary.PassCount)
	assert.Equal(t, 1, results.Summary.WarnCount)
	assert.Equal(t, 1, results.Summary.FailCount)
	assert.Equal(t, 1, results.Summary.ErrorCount)

	// Average confidence should be (0.9 + 0.8 + 0.7) / 3 = 0.8
	assert.InDelta(t, 0.8, results.Summary.Confidence, 0.01)
}

// Mock Agent for testing
type mockAgent struct {
	name      string
	available bool
	healthy   bool
	error     string
	results   []*AnalyzerResult
	duration  time.Duration
}

func (m *mockAgent) Name() string {
	return m.name
}

func (m *mockAgent) IsAvailable() bool {
	return m.available
}

func (m *mockAgent) Capabilities() []string {
	return []string{"test-capability"}
}

func (m *mockAgent) HealthCheck(ctx context.Context) error {
	if !m.healthy {
		return errors.New(m.error)
	}
	return nil
}

func (m *mockAgent) Analyze(ctx context.Context, data []byte, analyzers []AnalyzerSpec) (*AgentResult, error) {
	if !m.available {
		return nil, errors.New("agent not available")
	}

	// Create results for each analyzer provided, plus the pre-configured results
	allResults := make([]*AnalyzerResult, 0, len(m.results)+len(analyzers))

	// Add pre-configured results (e.g., the "Success" result)
	allResults = append(allResults, m.results...)

	// Add a result for each analyzer spec provided
	for i, analyzer := range analyzers {
		result := &AnalyzerResult{
			IsPass:     true,
			Title:      fmt.Sprintf("Mock Analysis: %s", analyzer.Name),
			Message:    fmt.Sprintf("Mock agent processed analyzer %d successfully", i),
			Category:   analyzer.Category,
			Confidence: 0.9,
			AgentName:  m.name,
		}
		allResults = append(allResults, result)
	}

	return &AgentResult{
		Results: allResults,
		Metadata: AgentResultMetadata{
			Duration:      m.duration,
			AnalyzerCount: len(analyzers),
			Version:       "1.0.0",
		},
		Errors: nil,
	}, nil
}

func TestSupportBundleMetadata_JSON(t *testing.T) {
	metadata := &SupportBundleMetadata{
		CreatedAt: time.Now(),
		Version:   "1.0.0",
		ClusterInfo: &ClusterInfo{
			Version:   "1.24.0",
			Platform:  "kubernetes",
			NodeCount: 3,
		},
		NodeInfo: []NodeInfo{
			{
				Name:         "node1",
				Version:      "1.24.0",
				OS:           "linux",
				Architecture: "amd64",
			},
		},
		GeneratedBy: "test",
		Namespace:   "default",
		Labels: map[string]string{
			"test": "value",
		},
	}

	// Test JSON marshaling/unmarshaling
	data, err := json.Marshal(metadata)
	require.NoError(t, err)

	var unmarshaled SupportBundleMetadata
	err = json.Unmarshal(data, &unmarshaled)
	require.NoError(t, err)

	assert.Equal(t, metadata.Version, unmarshaled.Version)
	assert.Equal(t, metadata.GeneratedBy, unmarshaled.GeneratedBy)
	assert.Equal(t, metadata.Namespace, unmarshaled.Namespace)
	assert.Equal(t, metadata.Labels, unmarshaled.Labels)
	assert.NotNil(t, unmarshaled.ClusterInfo)
	assert.Len(t, unmarshaled.NodeInfo, 1)
}

func TestAnalysisResult_JSON(t *testing.T) {
	result := &AnalysisResult{
		Results: []*AnalyzerResult{
			{
				IsPass:     true,
				Title:      "Test Result",
				Message:    "Test message",
				AgentName:  "test-agent",
				Confidence: 0.9,
				Category:   "test",
				Insights:   []string{"test insight"},
			},
		},
		Remediation: []RemediationStep{
			{
				Description:   "Test remediation",
				Priority:      5,
				IsAutomatable: true,
			},
		},
		Summary: AnalysisSummary{
			TotalAnalyzers: 1,
			PassCount:      1,
			Duration:       "1s",
			AgentsUsed:     []string{"test-agent"},
		},
		Metadata: AnalysisMetadata{
			Timestamp:     time.Now(),
			EngineVersion: "1.0.0",
		},
	}

	// Test JSON marshaling/unmarshaling
	data, err := json.Marshal(result)
	require.NoError(t, err)

	var unmarshaled AnalysisResult
	err = json.Unmarshal(data, &unmarshaled)
	require.NoError(t, err)

	assert.Len(t, unmarshaled.Results, 1)
	assert.Len(t, unmarshaled.Remediation, 1)
	assert.Equal(t, result.Summary.TotalAnalyzers, unmarshaled.Summary.TotalAnalyzers)
	assert.Equal(t, result.Metadata.EngineVersion, unmarshaled.Metadata.EngineVersion)
}

func TestAnalysisEngine_ConvertAnalyzerToSpec_ErrorHandling(t *testing.T) {
	engine := NewAnalysisEngine()

	tests := []struct {
		name          string
		analyzer      *troubleshootv1beta2.Analyze
		expectError   bool
		expectedError string
	}{
		{
			name:          "nil analyzer",
			analyzer:      nil,
			expectError:   true,
			expectedError: "analyzer cannot be nil",
		},
		{
			name: "supported ClusterVersion analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				ClusterVersion: &troubleshootv1beta2.ClusterVersion{
					Outcomes: []*troubleshootv1beta2.Outcome{},
				},
			},
			expectError: false,
		},
		{
			name: "supported DeploymentStatus analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				DeploymentStatus: &troubleshootv1beta2.DeploymentStatus{
					Name:     "test-deployment",
					Outcomes: []*troubleshootv1beta2.Outcome{},
				},
			},
			expectError: false,
		},
		{
			name: "now supported TextAnalyze analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				TextAnalyze: &troubleshootv1beta2.TextAnalyze{
					CollectorName: "test-logs",
					FileName:      "test.log",
				},
			},
			expectError: false,
		},
		{
			name: "now supported NodeResources analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				NodeResources: &troubleshootv1beta2.NodeResources{},
			},
			expectError: false,
		},
		{
			name: "supported Postgres analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				Postgres: &troubleshootv1beta2.DatabaseAnalyze{
					CollectorName: "postgres",
					FileName:      "postgres.json",
				},
			},
			expectError: false,
		},
		{
			name: "supported YamlCompare analyzer",
			analyzer: &troubleshootv1beta2.Analyze{
				YamlCompare: &troubleshootv1beta2.YamlCompare{
					CollectorName: "config",
					FileName:      "config.yaml",
					Path:          "data",
					Value:         "expected",
				},
			},
			expectError: false,
		},
		{
			name:          "completely unknown analyzer type",
			analyzer:      &troubleshootv1beta2.Analyze{},
			expectError:   true,
			expectedError: "unknown analyzer type - this should not happen as all known types are now supported",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			spec, err := engine.(*DefaultAnalysisEngine).convertAnalyzerToSpec(tt.analyzer)

			if tt.expectError {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tt.expectedError)
				assert.Empty(t, spec.Name) // Should have empty spec on error
			} else {
				assert.NoError(t, err)
				assert.NotEmpty(t, spec.Name)
				assert.NotEmpty(t, spec.Type)
				assert.NotNil(t, spec.Config)
			}
		})
	}
}

func TestAnalysisEngine_Analyze_ComprehensiveAnalyzerSupport(t *testing.T) {
	engine := NewAnalysisEngine()

	// Register a mock agent
	mockAgent := &mockAgent{
		name:      "test-agent",
		available: true,
		healthy:   true,
		results: []*AnalyzerResult{
			{IsPass: true, Title: "Test Result", Message: "Success"},
		},
		duration: 100 * time.Millisecond,
	}

	err := engine.RegisterAgent("test-agent", mockAgent)
	require.NoError(t, err)

	// Create a mock bundle
	bundle := &SupportBundle{
		Metadata: &SupportBundleMetadata{
			CreatedAt: time.Now(),
			Version:   "test",
		},
		Files: make(map[string][]byte),
	}

	// Create analysis options with comprehensive analyzer types (all now supported!)
	opts := AnalysisOptions{
		Agents: []string{"test-agent"},
		CustomAnalyzers: []*troubleshootv1beta2.Analyze{
			// Cluster analyzers
			{
				ClusterVersion: &troubleshootv1beta2.ClusterVersion{
					Outcomes: []*troubleshootv1beta2.Outcome{},
				},
			},
			{
				NodeResources: &troubleshootv1beta2.NodeResources{},
			},
			// Workload analyzers
			{
				DeploymentStatus: &troubleshootv1beta2.DeploymentStatus{
					Name:     "test-deployment",
					Outcomes: []*troubleshootv1beta2.Outcome{},
				},
			},
			{
				StatefulsetStatus: &troubleshootv1beta2.StatefulsetStatus{
					Name:     "test-statefulset",
					Outcomes: []*troubleshootv1beta2.Outcome{},
				},
			},
			// Data analyzers
			{
				TextAnalyze: &troubleshootv1beta2.TextAnalyze{
					CollectorName: "test-logs",
					FileName:      "test.log",
				},
			},
			{
				YamlCompare: &troubleshootv1beta2.YamlCompare{
					CollectorName: "config",
					FileName:      "config.yaml",
					Path:          "data",
					Value:         "test",
				},
			},
			// Database analyzers
			{
				Postgres: &troubleshootv1beta2.DatabaseAnalyze{
					CollectorName: "postgres",
					FileName:      "postgres.json",
				},
			},
		},
	}

	// Run analysis - all analyzers should now be supported!
	result, err := engine.Analyze(context.Background(), bundle, opts)

	// Verify analysis completes successfully
	assert.NoError(t, err)
	assert.NotNil(t, result)

	// Should have results: 1 from mock agent + 7 analyzer results (all converted successfully)
	expectedResults := len(opts.CustomAnalyzers) + 1 // 7 analyzers + 1 mock agent result
	assert.Len(t, result.Results, expectedResults, "Expected results from mock agent + all %d analyzer conversions", len(opts.CustomAnalyzers))

	// Count results by type
	mockResults := 0
	analyzerResults := 0
	failureResults := 0

	for _, res := range result.Results {
		if res.Message == "Success" {
			mockResults++
		} else if res.AgentName == "local" {
			analyzerResults++
		} else if res.IsFail && strings.Contains(res.Title, "Conversion Failed") {
			failureResults++
		}
	}

	assert.Equal(t, 1, mockResults, "Should have 1 mock agent result")
	// Note: analyzerResults may be 0 if traditional analyzers fail due to missing files (expected)
	// The important thing is that we get results (success or failure) for all analyzers, not silent skips
	assert.Equal(t, 0, failureResults, "Should have no conversion failures - all analyzer types now supported")

	// Verify agent was used
	assert.Contains(t, result.Summary.AgentsUsed, "test-agent")

	// No fatal errors should be recorded
	assert.Equal(t, 0, len(result.Errors))

	// The key success metric: All analyzers produced results (not silently skipped)
	// Whether they pass/warn/fail depends on data availability, but they all get processed
	fmt.Printf("✅ SUCCESS: All %d analyzers processed and accounted for!\n", len(opts.CustomAnalyzers))
}
