package version

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestGetCurrentVersion(t *testing.T) {
	version := GetCurrentVersion()
	assert.NotEmpty(t, version)
	assert.Equal(t, CurrentVersion, version)
}

func TestCompareVersions(t *testing.T) {
	tests := []struct {
		name string
		v1   string
		v2   string
		want int
	}{
		{
			name: "v1 less than v2",
			v1:   "0.1.0",
			v2:   "0.2.0",
			want: -1,
		},
		{
			name: "v1 greater than v2",
			v1:   "1.0.0",
			v2:   "0.9.0",
			want: 1,
		},
		{
			name: "v1 equal to v2",
			v1:   "1.0.0",
			v2:   "1.0.0",
			want: 0,
		},
		{
			name: "with v prefix",
			v1:   "v0.1.0",
			v2:   "v0.2.0",
			want: -1,
		},
		{
			name: "mixed v prefix",
			v1:   "0.1.0",
			v2:   "v0.2.0",
			want: -1,
		},
		{
			name: "different major version",
			v1:   "1.0.0",
			v2:   "2.0.0",
			want: -1,
		},
		{
			name: "different minor version",
			v1:   "1.0.0",
			v2:   "1.1.0",
			want: -1,
		},
		{
			name: "different patch version",
			v1:   "1.0.0",
			v2:   "1.0.1",
			want: -1,
		},
		{
			name: "v1 has more parts",
			v1:   "1.0.0.1",
			v2:   "1.0.0",
			want: 1,
		},
		{
			name: "v2 has more parts",
			v1:   "1.0.0",
			v2:   "1.0.0.1",
			want: -1,
		},
		{
			name: "empty strings",
			v1:   "",
			v2:   "",
			want: 0,
		},
		{
			name: "v1 empty",
			v1:   "",
			v2:   "1.0.0",
			want: -1,
		},
		{
			name: "v2 empty",
			v1:   "1.0.0",
			v2:   "",
			want: 1,
		},
		{
			name: "non-numeric parts",
			v1:   "1.0.0-alpha",
			v2:   "1.0.0",
			want: 0,
		},
		{
			name: "large version numbers",
			v1:   "100.200.300",
			v2:   "99.201.300",
			want: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := CompareVersions(tt.v1, tt.v2)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestIsVersionUpdateAvailable(t *testing.T) {
	tests := []struct {
		name       string
		current    string
		latest     string
		wantUpdate bool
	}{
		{
			name:       "update available",
			current:    "0.1.0",
			latest:     "0.2.0",
			wantUpdate: true,
		},
		{
			name:       "no update available",
			current:    "1.0.0",
			latest:     "0.9.0",
			wantUpdate: false,
		},
		{
			name:       "same version",
			current:    "1.0.0",
			latest:     "1.0.0",
			wantUpdate: false,
		},
		{
			name:       "major version update",
			current:    "1.0.0",
			latest:     "2.0.0",
			wantUpdate: true,
		},
		{
			name:       "patch update",
			current:    "1.0.0",
			latest:     "1.0.1",
			wantUpdate: true,
		},
		{
			name:       "with v prefix",
			current:    "v0.1.0",
			latest:     "v0.2.0",
			wantUpdate: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := IsVersionUpdateAvailable(tt.current, tt.latest)
			assert.Equal(t, tt.wantUpdate, got)
		})
	}
}

func TestCheckForUpdate(t *testing.T) {
	t.Run("check for update", func(t *testing.T) {
		result := CheckForUpdate()
		assert.NotNil(t, result)
		assert.Equal(t, CurrentVersion, result.CurrentVersion)

		if result.Error != nil {
			assert.Contains(t, result.Error.Error(), "failed to check for updates")
		}
	})
}

func TestVersionCheckResult(t *testing.T) {
	t.Run("create version check result", func(t *testing.T) {
		result := &VersionCheckResult{
			CurrentVersion:  "0.1.0",
			LatestVersion:   "0.2.0",
			MpvVersion:      "0.36.0",
			UpdateAvailable: true,
			URL:             "https://example.com/update",
			BLAKE3:          "blake3:abc123",
		}

		assert.Equal(t, "0.1.0", result.CurrentVersion)
		assert.Equal(t, "0.2.0", result.LatestVersion)
		assert.Equal(t, "0.36.0", result.MpvVersion)
		assert.True(t, result.UpdateAvailable)
		assert.Equal(t, "https://example.com/update", result.URL)
		assert.Equal(t, "blake3:abc123", result.BLAKE3)
		assert.Nil(t, result.Error)
	})
}

func TestProgressWriter(t *testing.T) {
	t.Run("Write updates written field", func(t *testing.T) {
		var buf bytes.Buffer
		pw := &progressWriter{
			total:      100,
			underlying: &buf,
		}

		n, err := pw.Write([]byte("hello"))
		assert.NoError(t, err)
		assert.Equal(t, 5, n)
		assert.Equal(t, int64(5), pw.written)
		assert.Equal(t, "hello", buf.String())
	})

	t.Run("Callback is called when throttle exceeded", func(t *testing.T) {
		var buf bytes.Buffer
		callbackCalled := 0
		var lastWritten, lastTotal int64

		pw := &progressWriter{
			total:      100,
			underlying: &buf,
			callback: func(written, total int64) {
				callbackCalled++
				lastWritten = written
				lastTotal = total
			},
			lastUpdate: time.Now().Add(-600 * time.Millisecond),
		}

		pw.Write([]byte("test"))
		assert.Equal(t, 1, callbackCalled)
		assert.Equal(t, int64(4), lastWritten)
		assert.Equal(t, int64(100), lastTotal)
	})

	t.Run("Callback is not called when throttle not exceeded", func(t *testing.T) {
		var buf bytes.Buffer
		callbackCalled := false

		pw := &progressWriter{
			total:      100,
			underlying: &buf,
			callback: func(written, total int64) {
				callbackCalled = true
			},
			lastUpdate: time.Now(),
		}

		pw.Write([]byte("test"))
		assert.False(t, callbackCalled)
	})

	t.Run("Callback called when download completes", func(t *testing.T) {
		var buf bytes.Buffer
		callbackCalled := false

		pw := &progressWriter{
			total:      4,
			underlying: &buf,
			callback: func(written, total int64) {
				callbackCalled = true
			},
			lastUpdate: time.Now(),
		}

		pw.Write([]byte("test"))
		assert.True(t, callbackCalled)
	})

	t.Run("Error propagates from underlying writer", func(t *testing.T) {
		pw := &progressWriter{
			total:      100,
			underlying: &errorWriter{},
			callback:   func(written, total int64) {},
		}

		_, err := pw.Write([]byte("test"))
		assert.Error(t, err)
		assert.Equal(t, "simulated write error", err.Error())
	})

	t.Run("callback called even on subsequent write error", func(t *testing.T) {
		callbackCalled := false
		pw := &progressWriter{
			total:      100,
			underlying: &errorWriter{},
			callback: func(written, total int64) {
				callbackCalled = true
			},
			lastUpdate: time.Now().Add(-600 * time.Millisecond),
		}

		_, err := pw.Write([]byte("test"))
		assert.Error(t, err)
		assert.True(t, callbackCalled, "callback should be called with progress before error")
	})
}

type errorWriter struct{}

func (e *errorWriter) Write(p []byte) (n int, err error) {
	return 0, fmt.Errorf("simulated write error")
}

func TestDownloadFileWithProgress(t *testing.T) {
	t.Run("successful download with progress", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("test file content"))
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "test-file.bin")

		progressUpdates := []struct{ written, total int64 }{}
		err := downloadFileWithProgress(server.URL, destPath, func(written, total int64) {
			progressUpdates = append(progressUpdates, struct{ written, total int64 }{written, total})
		}, 3)

		assert.NoError(t, err)
		assert.FileExists(t, destPath)

		content, _ := os.ReadFile(destPath)
		assert.Equal(t, "test file content", string(content))
		assert.NotEmpty(t, progressUpdates, "progress callback should be called")
	})

	t.Run("retry on connection error", func(t *testing.T) {
		attempts := 0
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			attempts++
			if attempts < 3 {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("success"))
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "retry-test.bin")

		err := downloadFileWithProgress(server.URL, destPath, nil, 3)
		assert.NoError(t, err)
		assert.Equal(t, 3, attempts)

		content, _ := os.ReadFile(destPath)
		assert.Equal(t, "success", string(content))
	})

	t.Run("max retries exceeded", func(t *testing.T) {
		attempts := 0
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			attempts++
			w.WriteHeader(http.StatusInternalServerError)
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "fail-test.bin")

		err := downloadFileWithProgress(server.URL, destPath, nil, 3)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "download failed after 3 attempts")
		assert.Equal(t, 3, attempts)
	})

	t.Run("HTTP 404 error", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusNotFound)
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "404-test.bin")

		err := downloadFileWithProgress(server.URL, destPath, nil, 1)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "download failed")
	})

	t.Run("file write error", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("test content"))
		}))
		defer server.Close()

		// Create a temporary directory with a subdirectory
		tmpDir := t.TempDir()
		dirPath := filepath.Join(tmpDir, "directory")
		err := os.Mkdir(dirPath, 0755)
		require.NoError(t, err)

		// Try to write to a path that's a directory (will fail)
		// os.Create() cannot create a file if a directory with that name already exists
		destPath := dirPath

		err = downloadFileWithProgress(server.URL, destPath, nil, 1)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "is a directory")
	})

	t.Run("exponential backoff delays", func(t *testing.T) {
		attempts := 0
		startTimes := []time.Time{}

		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			startTimes = append(startTimes, time.Now())
			attempts++
			if attempts < 4 {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("success"))
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "backoff-test.bin")

		_ = downloadFileWithProgress(server.URL, destPath, nil, 4)

		assert.Equal(t, 4, attempts)
		if len(startTimes) >= 2 {
			delay1 := startTimes[1].Sub(startTimes[0])
			assert.True(t, delay1 >= 900*time.Millisecond && delay1 <= 1100*time.Millisecond,
				fmt.Sprintf("first delay should be ~1s, got %v", delay1))
		}
		if len(startTimes) >= 3 {
			delay2 := startTimes[2].Sub(startTimes[1])
			assert.True(t, delay2 >= 1900*time.Millisecond && delay2 <= 2100*time.Millisecond,
				fmt.Sprintf("second delay should be ~2s, got %v", delay2))
		}
	})

	t.Run("progress callback frequency", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(strings.Repeat("test", 100)))
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "progress-test.bin")

		progressCount := 0
		_ = downloadFileWithProgress(server.URL, destPath, func(written, total int64) {
			progressCount++
		}, 1)

		assert.Greater(t, progressCount, 0, "progress callback should be called at least once")
	})

	t.Run("empty download", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
		}))
		defer server.Close()

		tmpDir := t.TempDir()
		destPath := filepath.Join(tmpDir, "empty-test.bin")

		progressUpdates := []struct{ written, total int64 }{}
		err := downloadFileWithProgress(server.URL, destPath, func(written, total int64) {
			progressUpdates = append(progressUpdates, struct{ written, total int64 }{written, total})
		}, 1)

		assert.NoError(t, err)
		assert.FileExists(t, destPath)

		content, _ := os.ReadFile(destPath)
		assert.Equal(t, 0, len(content))
		assert.Empty(t, progressUpdates)
	})
}

func TestUpdateSelfWithProgress(t *testing.T) {
	t.Run("function accepts callback parameter", func(t *testing.T) {
		tmpDir := t.TempDir()
		executablePath := filepath.Join(tmpDir, "mpv-manager-test")

		err := UpdateSelfWithProgress(executablePath, func(written, total int64) {})
		assert.Error(t, err)
	})

	t.Run("callback receives int64 parameters", func(t *testing.T) {
		tmpDir := t.TempDir()
		executablePath := filepath.Join(tmpDir, "mpv-manager-test")

		err := UpdateSelfWithProgress(executablePath, func(written, total int64) {
			assert.IsType(t, int64(0), written)
			assert.IsType(t, int64(0), total)
		})
		assert.Error(t, err)
	})
}
