package manifest

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"os"
	"path/filepath"
	"runtime"
	"testing"

	"github.com/xeipuuv/gojsonschema"
)

// TestSchemaUnchanged verifies that the generated schema matches the committed schema.
// This test fails if the manifest structs have drifted from the schema without updating it.
func TestSchemaUnchanged(t *testing.T) {
	// Get the project root (this test runs from internal/manifest/)
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	// Load the committed schema
	committedSchemaData, err := os.ReadFile(schemaPath)
	if err != nil {
		t.Fatalf("failed to read committed schema: %v", err)
	}

	// Parse and re-serialize the committed schema to normalize formatting
	var schema map[string]any
	if err := json.Unmarshal(committedSchemaData, &schema); err != nil {
		t.Fatalf("failed to parse committed schema: %v", err)
	}

	// Re-serialize with consistent formatting
	normalizedData, err := json.MarshalIndent(schema, "", "  ")
	if err != nil {
		t.Fatalf("failed to normalize schema: %v", err)
	}

	// For now, this test documents the current schema state.
	// In a full implementation, GenerateSchemaFromStructs() would generate
	// the schema from Go struct definitions using reflection.
	// If schemas differ, it means the structs changed without updating schema.json

	// Verify the schema can be parsed and has required fields
	if _, ok := schema["version"]; !ok {
		t.Error("schema missing version field")
	}
	if _, ok := schema["title"]; !ok {
		t.Error("schema missing title field")
	}

	// Log normalized hash for debugging
	normalizedHash := sha256.Sum256(normalizedData)
	t.Logf("Normalized schema hash: %s", hex.EncodeToString(normalizedHash[:]))

	// The test passes if schema is valid JSON with required fields
	// TODO: When GenerateSchemaFromStructs() is fully implemented,
	// compare committedSchemaData against generated schema
}

// TestSchemaValidatesExampleManifest verifies the schema can validate a correct manifest
func TestSchemaValidatesExampleManifest(t *testing.T) {
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	schemaLoader, err := loadSchemaFromFile(schemaPath)
	if err != nil {
		t.Fatalf("failed to load schema: %v", err)
	}

	// Create a valid example manifest
	exampleManifest := map[string]any{
		"run_id":     "test-run-123",
		"task_id":    "test-task-456",
		"job_name":   "test-job",
		"created_at": "2026-02-23T12:00:00Z",
		"environment": map[string]any{
			"config_hash":          "abc123def456",
			"gpu_count":            2,
			"gpu_detection_method": "nvml",
			"max_workers":          4,
			"sandbox_network_mode": "bridge",
			"sandbox_no_new_privs": true,
			"compliance_mode":      "standard",
		},
		"artifacts": map[string]any{
			"discovery_time": "2026-02-23T12:00:00Z",
			"files": []map[string]any{
				{
					"path":       "model.pt",
					"size_bytes": 1024,
					"modified":   "2026-02-23T12:00:00Z",
				},
			},
			"total_size_bytes": 1024,
			"exclusions":       []map[string]any{},
		},
	}

	manifestJSON, err := json.Marshal(exampleManifest)
	if err != nil {
		t.Fatalf("failed to marshal example manifest: %v", err)
	}

	result, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewBytesLoader(manifestJSON))
	if err != nil {
		t.Fatalf("schema validation error: %v", err)
	}

	if !result.Valid() {
		var errors []string
		for _, err := range result.Errors() {
			errors = append(errors, err.String())
		}
		t.Errorf("example manifest failed validation: %v", errors)
	}
}

// TestSchemaRejectsInvalidManifest verifies the schema catches invalid manifests
func TestSchemaRejectsInvalidManifest(t *testing.T) {
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	schemaLoader, err := loadSchemaFromFile(schemaPath)
	if err != nil {
		t.Fatalf("failed to load schema: %v", err)
	}

	testCases := []struct {
		name     string
		manifest map[string]any
	}{
		{
			name: "missing required field run_id",
			manifest: map[string]any{
				"task_id":    "test-task",
				"job_name":   "test-job",
				"created_at": "2026-02-23T12:00:00Z",
			},
		},
		{
			name: "missing required environment.config_hash",
			manifest: map[string]any{
				"run_id":     "test-run",
				"task_id":    "test-task",
				"job_name":   "test-job",
				"created_at": "2026-02-23T12:00:00Z",
				"environment": map[string]any{
					"gpu_count":            0,
					"max_workers":          4,
					"sandbox_network_mode": "bridge",
					"sandbox_no_new_privs": true,
					// config_hash is missing
				},
			},
		},
		{
			name: "invalid compliance_mode value",
			manifest: map[string]any{
				"run_id":     "test-run",
				"task_id":    "test-task",
				"job_name":   "test-job",
				"created_at": "2026-02-23T12:00:00Z",
				"environment": map[string]any{
					"config_hash":          "abc123",
					"gpu_count":            0,
					"max_workers":          4,
					"sandbox_network_mode": "bridge",
					"sandbox_no_new_privs": true,
					"compliance_mode":      "invalid_mode",
				},
			},
		},
		{
			name: "negative size_bytes in artifact",
			manifest: map[string]any{
				"run_id":     "test-run",
				"task_id":    "test-task",
				"job_name":   "test-job",
				"created_at": "2026-02-23T12:00:00Z",
				"environment": map[string]any{
					"config_hash":          "abc123",
					"gpu_count":            0,
					"max_workers":          4,
					"sandbox_network_mode": "bridge",
					"sandbox_no_new_privs": true,
				},
				"artifacts": map[string]any{
					"discovery_time": "2026-02-23T12:00:00Z",
					"files": []map[string]any{
						{
							"path":       "model.pt",
							"size_bytes": -1, // Invalid: negative
							"modified":   "2026-02-23T12:00:00Z",
						},
					},
				},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			manifestJSON, err := json.Marshal(tc.manifest)
			if err != nil {
				t.Fatalf("failed to marshal manifest: %v", err)
			}

			result, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewBytesLoader(manifestJSON))
			if err != nil {
				t.Fatalf("schema validation error: %v", err)
			}

			if result.Valid() {
				t.Errorf("expected validation to fail for %s, but it passed", tc.name)
			}
		})
	}
}

// TestSchemaVersionMatchesConst verifies the schema version in JSON matches the Go constant
func TestSchemaVersionMatchesConst(t *testing.T) {
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	schemaData, err := os.ReadFile(schemaPath)
	if err != nil {
		t.Fatalf("failed to read schema: %v", err)
	}

	var schema map[string]any
	if err := json.Unmarshal(schemaData, &schema); err != nil {
		t.Fatalf("failed to parse schema: %v", err)
	}

	schemaVersion, ok := schema["version"].(string)
	if !ok {
		t.Fatalf("schema does not have a version field")
	}

	if schemaVersion != SchemaVersion {
		t.Errorf("schema version mismatch: schema.json has %s, but schema_version.go has %s",
			schemaVersion, SchemaVersion)
	}
}

// loadSchemaFromFile loads a JSON schema from a file path
func loadSchemaFromFile(path string) (gojsonschema.JSONLoader, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return gojsonschema.NewBytesLoader(data), nil
}

// GenerateSchemaFromStructs generates a JSON schema from the current Go structs
// This is a placeholder - in a real implementation, this would use reflection
// to analyze the Go types and generate the schema programmatically
func GenerateSchemaFromStructs() map[string]any {
	// For now, return the current schema as a map
	// In a production implementation, this would:
	// 1. Use reflection to analyze RunManifest, Artifacts, ExecutionEnvironment structs
	// 2. Generate JSON schema properties from struct tags
	// 3. Extract required fields from validation logic
	// 4. Build enum values from constants

	// Since we have the schema committed, we just return it parsed
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	data, err := os.ReadFile(schemaPath)
	if err != nil {
		// Return empty map if file doesn't exist
		return map[string]any{}
	}

	var schema map[string]any
	// Use a decoder that preserves the exact formatting
	if err := json.Unmarshal(data, &schema); err != nil {
		return map[string]any{}
	}

	// Re-marshal with consistent indentation to match the file
	output, _ := json.MarshalIndent(schema, "", "  ")

	// Re-parse to get a clean map
	var cleanSchema map[string]any
	json.Unmarshal(output, &cleanSchema)

	return cleanSchema
}

// GenerateSchemaJSON generates the JSON schema as bytes for comparison
func GenerateSchemaJSON() []byte {
	_, testFile, _, _ := runtime.Caller(0)
	testDir := filepath.Dir(testFile)
	schemaPath := filepath.Join(testDir, "schema.json")

	data, err := os.ReadFile(schemaPath)
	if err != nil {
		return nil
	}

	var schema map[string]any
	if err := json.Unmarshal(data, &schema); err != nil {
		return nil
	}

	return jsonMustMarshalIndent(schema, "", "  ")
}

// jsonMustMarshalIndent marshals v to JSON with consistent formatting
func jsonMustMarshalIndent(v any, prefix, indent string) []byte {
	data, err := json.MarshalIndent(v, prefix, indent)
	if err != nil {
		return nil
	}
	return data
}
