package model

import (
	"testing"
)

func TestBeaconEventHash(t *testing.T) {
	tests := []struct {
		name     string
		be       BeaconEvent
		expected []byte
	}{
		{
			name: "Basic beacon event",
			be: BeaconEvent{
				ID:      "beacon-1",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1000,
				Event:   1,
			},
			expected: nil, // We'll test that it produces a consistent hash
		},
		{
			name: "Same beacon with different battery should produce same hash",
			be: BeaconEvent{
				ID:      "beacon-1",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1009, // 1000 + 9, should round to 1000
				Event:   1,
			},
			expected: nil,
		},
		{
			name: "Different ID should produce different hash",
			be: BeaconEvent{
				ID:      "beacon-2",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1000,
				Event:   1,
			},
			expected: nil,
		},
		{
			name: "Different event should produce different hash",
			be: BeaconEvent{
				ID:      "beacon-1",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1000,
				Event:   2,
			},
			expected: nil,
		},
		{
			name: "Zero values",
			be: BeaconEvent{
				ID:      "",
				Name:    "",
				Type:    "",
				Battery: 0,
				Event:   0,
			},
			expected: nil,
		},
		{
			name: "Special characters",
			be: BeaconEvent{
				ID:      "beacon!@#$%^&*()",
				Name:    "Test\nBeacon\tWith\tTabs",
				Type:    "Special-Type_123",
				Battery: 1000,
				Event:   1,
			},
			expected: nil,
		},
	}

	// Test that Hash produces consistent results
	hashes := make([][]byte, len(tests))

	for i, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			hashes[i] = tt.be.Hash()

			// Hash should always be 32 bytes for SHA256
			if len(hashes[i]) != 32 {
				t.Errorf("Hash() length = %v, expected 32", len(hashes[i]))
			}

			// Hash should not be empty unless all fields are empty
			if len(hashes[i]) == 0 && (tt.be.ID != "" || tt.be.Name != "" || tt.be.Type != "") {
				t.Errorf("Hash() should not be empty for non-empty beacon event")
			}
		})
	}

	// Test that same input produces same hash
	be1 := BeaconEvent{
		ID:      "test-beacon",
		Name:    "Test",
		Type:    "Ingics",
		Battery: 1000,
		Event:   1,
	}

	hash1 := be1.Hash()
	hash2 := be1.Hash()

	if string(hash1) != string(hash2) {
		t.Errorf("Hash() should be deterministic: %v != %v", hash1, hash2)
	}

	// Test battery rounding
	beBattery1 := BeaconEvent{
		ID:      "test-beacon",
		Name:    "Test",
		Type:    "Ingics",
		Battery: 1005, // Should round to 1000
		Event:   1,
	}

	beBattery2 := BeaconEvent{
		ID:      "test-beacon",
		Name:    "Test",
		Type:    "Ingics",
		Battery: 1000,
		Event:   1,
	}

	hashBattery1 := beBattery1.Hash()
	hashBattery2 := beBattery2.Hash()

	if string(hashBattery1) != string(hashBattery2) {
		t.Errorf("Hash() with battery rounding should be same: %v != %v", hashBattery1, hashBattery2)
	}
}

func TestBeaconEventToJSON(t *testing.T) {
	tests := []struct {
		name          string
		be            BeaconEvent
		expectedError bool
	}{
		{
			name: "Valid beacon event",
			be: BeaconEvent{
				ID:      "beacon-1",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1000,
				Event:   1,
			},
			expectedError: false,
		},
		{
			name: "Empty beacon event",
			be: BeaconEvent{},
			expectedError: false,
		},
		{
			name: "Beacon with special characters",
			be: BeaconEvent{
				ID:      "beacon-with-special-chars!@#$%",
				Name:    "Name with unicode: 测试",
				Type:    "Type-With-Dashes_and_underscores",
				Battery: 12345,
				Event:   255,
			},
			expectedError: false,
		},
		{
			name: "Beacon with maximum values",
			be: BeaconEvent{
				ID:      "max-beacon",
				Name:    "Maximum Values Test",
				Type:    "MaxType",
				Battery: 0xFFFFFFFF, // Max uint32
				Event:   2147483647, // Max int32
			},
			expectedError: false,
		},
		{
			name: "Zero values",
			be: BeaconEvent{
				ID:      "",
				Name:    "",
				Type:    "",
				Battery: 0,
				Event:   0,
			},
			expectedError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := tt.be.ToJSON()

			if (err != nil) != tt.expectedError {
				t.Errorf("ToJSON() error = %v, expectedError %v", err, tt.expectedError)
				return
			}

			if !tt.expectedError {
				// Result should not be nil
				if result == nil {
					t.Error("ToJSON() result should not be nil")
					return
				}

				// Result should not be empty JSON
				if string(result) == "null" {
					t.Error("ToJSON() result should not be 'null'")
				}

				// Basic JSON validation - should start and end with braces for object
				if len(result) > 0 && result[0] != '{' && result[0] != '[' {
					t.Errorf("ToJSON() result should be valid JSON, got: %s", string(result))
				}
			}
		})
	}
}

func TestConvertStructToMap(t *testing.T) {
	tests := []struct {
		name          string
		input         any
		expectedError bool
	}{
		{
			name:          "Valid BeaconEvent",
			input:         BeaconEvent{ID: "test", Type: "Ingics"},
			expectedError: false,
		},
		{
			name:          "Valid HTTPLocation",
			input:         HTTPLocation{Method: "POST", ID: "test"},
			expectedError: false,
		},
		{
			name:          "Valid struct",
			input:         struct{ Name string }{Name: "test"},
			expectedError: false,
		},
		{
			name:          "Nil input",
			input:         nil,
			expectedError: false,
		},
		{
			name:          "String input",
			input:         "test string",
			expectedError: false,
		},
		{
			name:          "Map input",
			input:         map[string]any{"test": "value"},
			expectedError: false,
		},
		{
			name:          "Slice input",
			input:         []string{"test1", "test2"},
			expectedError: false,
		},
		{
			name:          "Complex struct with nested structures",
			input: struct {
				SimpleField string
				Nested      struct {
					InnerField int
				}
				SliceField []string
				MapField   map[string]any
			}{
				SimpleField: "test",
				Nested:      struct{ InnerField int }{InnerField: 123},
				SliceField:  []string{"a", "b", "c"},
				MapField:    map[string]any{"key": "value"},
			},
			expectedError: false,
		},
		{
			name:          "Struct with channel field",
			input:         struct{ Ch chan int }{Ch: make(chan int)},
			expectedError: true, // Channels cannot be marshaled to JSON
		},
		{
			name:          "Struct with function field",
			input:         struct{ Func func() }{Func: func() {}},
			expectedError: true, // Functions cannot be marshaled to JSON
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := convertStructToMap(tt.input)

			if (err != nil) != tt.expectedError {
				t.Errorf("convertStructToMap() error = %v, expectedError %v", err, tt.expectedError)
				return
			}

			if !tt.expectedError {
				// Result should be a map
				if result == nil && tt.input != nil {
					t.Error("convertStructToMap() result should not be nil for non-nil input")
				}

				// For valid inputs, result should be a map
				if tt.input != nil {
					if _, ok := result.(map[string]any); !ok && result != nil {
						t.Errorf("convertStructToMap() result should be a map[string]any, got %T", result)
					}
				}
			}
		})
	}
}

func TestHTTPLocationRedisHashable(t *testing.T) {
	tests := []struct {
		name          string
		location      HTTPLocation
		expectedError bool
	}{
		{
			name: "Valid location",
			location: HTTPLocation{
				Method:                    "POST",
				PreviousConfidentLocation: "room1",
				Distance:                  5.5,
				ID:                        "beacon-123",
				Location:                  "room2",
				LastSeen:                  1634567890,
			},
			expectedError: false,
		},
		{
			name: "Minimal location",
			location: HTTPLocation{
				Method: "GET",
				ID:     "beacon-1",
			},
			expectedError: false,
		},
		{
			name: "Zero values",
			location: HTTPLocation{},
			expectedError: false,
		},
		{
			name: "Location with special characters",
			location: HTTPLocation{
				Method:   "CUSTOM",
				ID:       "beacon-with-special-chars!@#$%",
				Location: "Room-with-unicode: 测试",
				Distance: -123.456, // Negative distance
			},
			expectedError: false,
		},
		{
			name: "Maximum values",
			location: HTTPLocation{
				Method:                    "MAX",
				PreviousConfidentLocation: "max-room",
				Distance:                  9223372036854775807, // Max int64 as float64
				ID:                        "max-beacon-id-12345678901234567890",
				Location:                  "max-location-name",
				LastSeen:                  9223372036854775807, // Max int64
			},
			expectedError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := tt.location.RedisHashable()

			if (err != nil) != tt.expectedError {
				t.Errorf("HTTPLocation.RedisHashable() error = %v, expectedError %v", err, tt.expectedError)
				return
			}

			if !tt.expectedError {
				// Result should be a map
				if result == nil {
					t.Error("HTTPLocation.RedisHashable() result should not be nil")
					return
				}

				resultMap, ok := result.(map[string]any)
				if !ok {
					t.Errorf("HTTPLocation.RedisHashable() result should be a map[string]any, got %T", result)
					return
				}

				// Check that expected fields are present
				expectedFields := []string{"method", "previous_confident_location", "distance", "id", "location", "last_seen"}
				for _, field := range expectedFields {
					if _, exists := resultMap[field]; !exists {
						t.Errorf("HTTPLocation.RedisHashable() missing expected field: %s", field)
					}
				}

				// Check JSON tags are respected
				if _, exists := resultMap["Method"]; exists {
					t.Error("HTTPLocation.RedisHashable() should use JSON field names, not struct field names")
				}

				if _, exists := resultMap["method"]; !exists {
					t.Error("HTTPLocation.RedisHashable() should contain 'method' field (JSON tag)")
				}
			}
		})
	}
}

func TestBeaconEventRedisHashable(t *testing.T) {
	tests := []struct {
		name          string
		be            BeaconEvent
		expectedError bool
	}{
		{
			name: "Valid beacon event",
			be: BeaconEvent{
				ID:      "beacon-123",
				Name:    "Test Beacon",
				Type:    "Ingics",
				Battery: 1000,
				Event:   1,
			},
			expectedError: false,
		},
		{
			name: "Minimal beacon event",
			be: BeaconEvent{
				ID: "test",
			},
			expectedError: false,
		},
		{
			name: "Zero values",
			be: BeaconEvent{},
			expectedError: false,
		},
		{
			name: "Beacon event with special characters",
			be: BeaconEvent{
				ID:      "beacon-!@#$%^&*()",
				Name:    "Name with unicode: 测试",
				Type:    "Special-Type_123",
				Battery: 12345,
				Event:   255,
			},
			expectedError: false,
		},
		{
			name: "Maximum values",
			be: BeaconEvent{
				ID:      "max-beacon-id",
				Name:    "Maximum Values Test",
				Type:    "MaxType",
				Battery: 0xFFFFFFFF, // Max uint32
				Event:   2147483647, // Max int32
			},
			expectedError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := tt.be.RedisHashable()

			if (err != nil) != tt.expectedError {
				t.Errorf("BeaconEvent.RedisHashable() error = %v, expectedError %v", err, tt.expectedError)
				return
			}

			if !tt.expectedError {
				// Result should be a map
				if result == nil {
					t.Error("BeaconEvent.RedisHashable() result should not be nil")
					return
				}

				resultMap, ok := result.(map[string]any)
				if !ok {
					t.Errorf("BeaconEvent.RedisHashable() result should be a map[string]any, got %T", result)
					return
				}

				// Check that expected fields are present
				expectedFields := []string{"name", "id", "type", "battery", "event"}
				for _, field := range expectedFields {
					if _, exists := resultMap[field]; !exists {
						t.Errorf("BeaconEvent.RedisHashable() missing expected field: %s", field)
					}
				}

				// Check JSON tags are respected (BeaconEvent fields are not tagged with JSON, so field names should be lowercase)
				if _, exists := resultMap["Name"]; exists {
					t.Error("BeaconEvent.RedisHashable() should use lowercase field names")
				}

				if _, exists := resultMap["name"]; !exists {
					t.Error("BeaconEvent.RedisHashable() should contain 'name' field")
				}
			}
		})
	}
}

func TestHashConsistencyWithBatteryRounding(t *testing.T) {
	// Test that Hash() is consistent with battery rounding
	testCases := []struct {
		battery1 uint32
		battery2 uint32
		shouldMatch bool
	}{
		{1000, 1009, true},  // Same rounding range
		{1000, 1010, false}, // Different rounding range
		{0, 9, true},        // Zero range
		{100, 104, true},    // Same range (100-109 rounds to 100)
		{100, 110, false},   // Different ranges
		{4294967295, 4294967289, true}, // Max value range
	}

	for i, tc := range testCases {
		t.Run(fmt.Sprintf("BatteryRoundCase_%d", i), func(t *testing.T) {
			be1 := BeaconEvent{
				ID:      "test-beacon",
				Name:    "Test",
				Type:    "Ingics",
				Battery: tc.battery1,
				Event:   1,
			}

			be2 := BeaconEvent{
				ID:      "test-beacon",
				Name:    "Test",
				Type:    "Ingics",
				Battery: tc.battery2,
				Event:   1,
			}

			hash1 := be1.Hash()
			hash2 := be2.Hash()

			hashesMatch := string(hash1) == string(hash2)

			if hashesMatch != tc.shouldMatch {
				t.Errorf("Hash consistency mismatch: battery1=%d, battery2=%d, hashesMatch=%v, shouldMatch=%v",
					tc.battery1, tc.battery2, hashesMatch, tc.shouldMatch)
			}
		})
	}
}

func TestJSONMarshalUnmarshalRoundtrip(t *testing.T) {
	original := BeaconEvent{
		ID:      "roundtrip-test",
		Name:    "Roundtrip Test",
		Type:    "TestType",
		Battery: 12345,
		Event:   42,
	}

	// Test that ToJSON produces valid JSON that can be unmarshaled back
	jsonData, err := original.ToJSON()
	if err != nil {
		t.Fatalf("ToJSON() error: %v", err)
	}

	var unmarshaled BeaconEvent
	err = json.Unmarshal(jsonData, &unmarshaled)
	if err != nil {
		t.Fatalf("json.Unmarshal() error: %v", err)
	}

	// Verify roundtrip integrity
	if unmarshaled.ID != original.ID {
		t.Errorf("Roundtrip ID mismatch: got %v, want %v", unmarshaled.ID, original.ID)
	}
	if unmarshaled.Name != original.Name {
		t.Errorf("Roundtrip Name mismatch: got %v, want %v", unmarshaled.Name, original.Name)
	}
	if unmarshaled.Type != original.Type {
		t.Errorf("Roundtrip Type mismatch: got %v, want %v", unmarshaled.Type, original.Type)
	}
	if unmarshaled.Battery != original.Battery {
		t.Errorf("Roundtrip Battery mismatch: got %v, want %v", unmarshaled.Battery, original.Battery)
	}
	if unmarshaled.Event != original.Event {
		t.Errorf("Roundtrip Event mismatch: got %v, want %v", unmarshaled.Event, original.Event)
	}
}

// Benchmark tests
func BenchmarkHash(b *testing.B) {
	be := BeaconEvent{
		ID:      "benchmark-beacon",
		Name:    "Benchmark Test",
		Type:    "Ingics",
		Battery: 1000,
		Event:   1,
	}

	for i := 0; i < b.N; i++ {
		be.Hash()
	}
}

func BenchmarkToJSON(b *testing.B) {
	be := BeaconEvent{
		ID:      "benchmark-beacon",
		Name:    "Benchmark Test",
		Type:    "Ingics",
		Battery: 1000,
		Event:   1,
	}

	for i := 0; i < b.N; i++ {
		be.ToJSON()
	}
}

func BenchmarkConvertStructToMap(b *testing.B) {
	be := BeaconEvent{
		ID:      "benchmark-beacon",
		Name:    "Benchmark Test",
		Type:    "Ingics",
		Battery: 1000,
		Event:   1,
	}

	for i := 0; i < b.N; i++ {
		convertStructToMap(be)
	}
}