// Package onviftesting provides testing utilities for ONVIF client testing.
package onviftesting

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// GoldenManifest describes a camera's golden file set.
type GoldenManifest struct {
	Version        string         `json:"version"`
	Camera         CameraInfo     `json:"camera"`
	CaptureDate    string         `json:"capture_date"`
	Capabilities   []string       `json:"capabilities"`
	OperationCount map[string]int `json:"operation_count"`
	Notes          string         `json:"notes,omitempty"`
}

// GoldenFile represents a single operation's expected result.
type GoldenFile struct {
	Operation      string                 `json:"operation"`
	Service        string                 `json:"service"`
	Parameters     map[string]string      `json:"parameters,omitempty"`
	Request        string                 `json:"request"`
	Response       string                 `json:"response"`
	ExpectedFields map[string]interface{} `json:"expected_fields,omitempty"`
	VariableFields []string               `json:"variable_fields,omitempty"`
}

// GoldenFileSet holds all golden files for a camera.
type GoldenFileSet struct {
	Manifest *GoldenManifest
	Files    map[string]*GoldenFile // key is operation + params
	BasePath string
}

// LoadGoldenManifest loads a manifest.json from a golden directory.
func LoadGoldenManifest(goldenDir string) (*GoldenManifest, error) {
	manifestPath := filepath.Join(goldenDir, "manifest.json")
	data, err := os.ReadFile(manifestPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read manifest: %w", err)
	}

	var manifest GoldenManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
	}

	return &manifest, nil
}

// LoadGoldenFiles loads all golden files from a camera directory.
func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) {
	set := &GoldenFileSet{
		Files:    make(map[string]*GoldenFile),
		BasePath: goldenDir,
	}

	// Load manifest if it exists
	manifest, err := LoadGoldenManifest(goldenDir)
	if err == nil {
		set.Manifest = manifest
	}

	// Walk through all JSON files in the directory
	err = filepath.Walk(goldenDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Skip directories and non-JSON files
		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		// Skip manifest.json
		if info.Name() == "manifest.json" {
			return nil
		}

		data, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("failed to read %s: %w", path, err)
		}

		var golden GoldenFile
		if err := json.Unmarshal(data, &golden); err != nil {
			return fmt.Errorf("failed to unmarshal %s: %w", path, err)
		}

		// Build key from operation and parameters
		key := buildGoldenKey(&golden)
		set.Files[key] = &golden

		return nil
	})

	if err != nil {
		return nil, err
	}

	return set, nil
}

// buildGoldenKey creates a unique key for a golden file.
func buildGoldenKey(g *GoldenFile) string {
	key := g.Operation
	if g.Parameters != nil {
		// Sort parameters for consistent keys
		for k, v := range g.Parameters {
			key += "_" + k + "_" + v
		}
	}
	return key
}

// GetGoldenFile retrieves a golden file by operation name and parameters.
func (s *GoldenFileSet) GetGoldenFile(operation string, params map[string]string) *GoldenFile {
	// Try exact match first
	golden := &GoldenFile{Operation: operation, Parameters: params}
	key := buildGoldenKey(golden)
	if g, ok := s.Files[key]; ok {
		return g
	}

	// Fall back to operation-only match
	for _, g := range s.Files {
		if g.Operation == operation {
			return g
		}
	}

	return nil
}

// GetOperations returns all unique operations in the golden file set.
func (s *GoldenFileSet) GetOperations() []string {
	seen := make(map[string]bool)
	var ops []string

	for _, g := range s.Files {
		if !seen[g.Operation] {
			seen[g.Operation] = true
			ops = append(ops, g.Operation)
		}
	}

	return ops
}

// ValidateResponse validates a response against expected fields in a golden file.
func ValidateResponse(response interface{}, golden *GoldenFile) []string {
	if golden.ExpectedFields == nil {
		return nil
	}

	var errors []string

	// Convert response to map for comparison
	responseData, err := toMap(response)
	if err != nil {
		return []string{fmt.Sprintf("failed to convert response: %v", err)}
	}

	// Check each expected field
	for field, expected := range golden.ExpectedFields {
		actual, ok := responseData[field]
		if !ok {
			errors = append(errors, fmt.Sprintf("missing field: %s", field))
			continue
		}

		// Skip variable fields (like timestamps)
		if isVariableField(field, golden.VariableFields) {
			continue
		}

		// Compare values
		if !valuesEqual(expected, actual) {
			errors = append(errors, fmt.Sprintf("field %s: expected %v, got %v", field, expected, actual))
		}
	}

	return errors
}

// toMap converts a struct to a map for field comparison.
func toMap(v interface{}) (map[string]interface{}, error) {
	data, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}

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

	return result, nil
}

// isVariableField checks if a field should be skipped during validation.
func isVariableField(field string, variableFields []string) bool {
	for _, v := range variableFields {
		if v == field {
			return true
		}
	}
	return false
}

// valuesEqual compares two values for equality.
func valuesEqual(expected, actual interface{}) bool {
	// Handle nil comparison
	if expected == nil && actual == nil {
		return true
	}
	if expected == nil || actual == nil {
		return false
	}

	// Convert to JSON for deep comparison
	e, err1 := json.Marshal(expected)
	a, err2 := json.Marshal(actual)
	if err1 != nil || err2 != nil {
		return false
	}

	return string(e) == string(a)
}

// SaveGoldenFile saves a golden file to disk.
func SaveGoldenFile(golden *GoldenFile, outputPath string) error {
	data, err := json.MarshalIndent(golden, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal golden file: %w", err)
	}

	// Create directory if needed
	dir := filepath.Dir(outputPath)
	if err := os.MkdirAll(dir, 0750); err != nil { //nolint:mnd
		return fmt.Errorf("failed to create directory: %w", err)
	}

	if err := os.WriteFile(outputPath, data, 0600); err != nil { //nolint:mnd
		return fmt.Errorf("failed to write file: %w", err)
	}

	return nil
}

// SaveGoldenManifest saves a manifest file to disk.
func SaveGoldenManifest(manifest *GoldenManifest, outputPath string) error {
	data, err := json.MarshalIndent(manifest, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal manifest: %w", err)
	}

	if err := os.WriteFile(outputPath, data, 0600); err != nil { //nolint:mnd
		return fmt.Errorf("failed to write manifest: %w", err)
	}

	return nil
}

// GenerateGoldenFileName generates a filename for a golden file.
func GenerateGoldenFileName(operation string, params map[string]string) string {
	name := operation
	for k, v := range params {
		// Sanitize parameter value for filename
		v = strings.ReplaceAll(v, "/", "_")
		v = strings.ReplaceAll(v, "\\", "_")
		name += "_" + k + "_" + v
	}
	return name + ".json"
}

// CreateGoldenFromCapture creates a golden file from a captured exchange.
func CreateGoldenFromCapture(exchange *CapturedExchangeV2) *GoldenFile {
	params := make(map[string]string)
	if exchange.Parameters != nil {
		for k, v := range exchange.Parameters {
			if s, ok := v.(string); ok {
				params[k] = s
			}
		}
	}

	return &GoldenFile{
		Operation:  exchange.OperationName,
		Service:    string(exchange.ServiceType),
		Parameters: params,
		Request:    exchange.RequestBody,
		Response:   exchange.ResponseBody,
	}
}

// GoldenTestRunner helps run tests against golden files.
type GoldenTestRunner struct {
	GoldenSet *GoldenFileSet
}

// NewGoldenTestRunner creates a new golden test runner.
func NewGoldenTestRunner(goldenDir string) (*GoldenTestRunner, error) {
	set, err := LoadGoldenFiles(goldenDir)
	if err != nil {
		return nil, err
	}

	return &GoldenTestRunner{GoldenSet: set}, nil
}

// ValidateOperation validates a response against the golden file for an operation.
func (r *GoldenTestRunner) ValidateOperation(operation string, params map[string]string, response interface{}) []string {
	golden := r.GoldenSet.GetGoldenFile(operation, params)
	if golden == nil {
		return []string{fmt.Sprintf("no golden file found for operation: %s", operation)}
	}

	return ValidateResponse(response, golden)
}
