package installer

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"strings"
	"testing"
)

func TestMockCommandExecutor_RecordsCommands(t *testing.T) {
	mock := NewMockCommandExecutor()

	err := mock.RunCommand("flatpak", "install", "-y", "io.mpv.Mpv")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	commands := mock.GetCommands()
	if len(commands) != 1 {
		t.Fatalf("expected 1 command, got %d", len(commands))
	}

	if commands[0].Name != "flatpak" {
		t.Errorf("expected command name 'flatpak', got %q", commands[0].Name)
	}

	expectedArgs := []string{"install", "-y", "io.mpv.Mpv"}
	if len(commands[0].Args) != len(expectedArgs) {
		t.Errorf("expected %d args, got %d", len(expectedArgs), len(commands[0].Args))
	}
	for i, arg := range expectedArgs {
		if commands[0].Args[i] != arg {
			t.Errorf("expected arg[%d] = %q, got %q", i, arg, commands[0].Args[i])
		}
	}
}

func TestMockCommandExecutor_CustomBehavior(t *testing.T) {
	mock := NewMockCommandExecutor()
	mock.RunCommandFunc = func(name string, args ...string) error {
		return fmt.Errorf("simulated failure")
	}

	err := mock.RunCommand("apt", "install", "mpv")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if err.Error() != "simulated failure" {
		t.Errorf("expected 'simulated failure', got %q", err.Error())
	}
}

func TestMockCommandExecutor_Cancellation(t *testing.T) {
	mock := NewMockCommandExecutor()
	ctx, cancel := context.WithCancel(context.Background())
	mock.SetContext(ctx)

	if mock.IsCancelled() {
		t.Fatal("should not be cancelled initially")
	}

	cancel()

	if !mock.IsCancelled() {
		t.Fatal("should be cancelled after cancel()")
	}

	err := mock.CheckCancelled()
	if err == nil {
		t.Fatal("CheckCancelled should return error when cancelled")
	}
}

func TestMockCommandExecutor_RunCommands(t *testing.T) {
	mock := NewMockCommandExecutor()

	commands := []string{
		"echo 'hello'",
		"echo 'world'",
	}

	err := mock.RunCommands(commands)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	shellCommands := mock.GetShellCommands()
	if len(shellCommands) != 2 {
		t.Fatalf("expected 2 shell commands, got %d", len(shellCommands))
	}
}

func TestMockCommandExecutor_RunCommands_WithError(t *testing.T) {
	mock := NewMockCommandExecutor()
	mock.RunShellFunc = func(script string) error {
		if strings.Contains(script, "fail") {
			return fmt.Errorf("command failed")
		}
		return nil
	}

	commands := []string{
		"echo 'hello'",
		"fail",
		"echo 'world'",
	}

	err := mock.RunCommands(commands)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	// Should have stopped after the failing command
	shellCommands := mock.GetShellCommands()
	if len(shellCommands) != 2 {
		t.Errorf("expected 2 shell commands (stopped at failure), got %d", len(shellCommands))
	}
}

func TestMockCommandExecutor_Reset(t *testing.T) {
	mock := NewMockCommandExecutor()

	mock.RunCommand("test", "arg1")
	mock.RunShellCommand("echo test")

	mock.Reset()

	commands := mock.GetCommands()
	if len(commands) != 0 {
		t.Errorf("expected 0 commands after reset, got %d", len(commands))
	}

	shellCommands := mock.GetShellCommands()
	if len(shellCommands) != 0 {
		t.Errorf("expected 0 shell commands after reset, got %d", len(shellCommands))
	}
}

func TestMockFileSystem_RecordsOperations(t *testing.T) {
	mock := NewMockFileSystem()

	// Test WriteFile
	err := mock.WriteFile("/test/file.txt", []byte("hello"), 0644)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Test Exists
	if !mock.Exists("/test/file.txt") {
		t.Error("file should exist")
	}

	// Test ReadFile
	data, err := mock.ReadFile("/test/file.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if string(data) != "hello" {
		t.Errorf("expected 'hello', got %q", string(data))
	}
}

func TestMockFileSystem_Directories(t *testing.T) {
	mock := NewMockFileSystem()

	// Test MkdirAll
	err := mock.MkdirAll("/test/nested/dir", 0755)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Test IsDir
	if !mock.IsDir("/test/nested/dir") {
		t.Error("path should be a directory")
	}

	// Test Exists for directory
	if !mock.Exists("/test/nested/dir") {
		t.Error("directory should exist")
	}
}

func TestMockFileSystem_Remove(t *testing.T) {
	mock := NewMockFileSystem()

	mock.WriteFile("/test/file.txt", []byte("content"), 0644)
	mock.MkdirAll("/test/dir", 0755)

	// Remove file
	err := mock.Remove("/test/file.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if mock.Exists("/test/file.txt") {
		t.Error("file should not exist after remove")
	}

	// Remove directory
	err = mock.Remove("/test/dir")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if mock.Exists("/test/dir") {
		t.Error("directory should not exist after remove")
	}
}

func TestMockFileSystem_RemoveAll(t *testing.T) {
	mock := NewMockFileSystem()

	mock.WriteFile("/test/file1.txt", []byte("content1"), 0644)
	mock.WriteFile("/test/file2.txt", []byte("content2"), 0644)
	mock.WriteFile("/test/nested/file3.txt", []byte("content3"), 0644)

	err := mock.RemoveAll("/test")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if mock.Exists("/test/file1.txt") {
		t.Error("file1 should not exist after RemoveAll")
	}
	if mock.Exists("/test/file2.txt") {
		t.Error("file2 should not exist after RemoveAll")
	}
	if mock.Exists("/test/nested/file3.txt") {
		t.Error("nested file should not exist after RemoveAll")
	}
}

func TestMockFileSystem_Rename(t *testing.T) {
	mock := NewMockFileSystem()

	mock.WriteFile("/test/old.txt", []byte("content"), 0644)

	err := mock.Rename("/test/old.txt", "/test/new.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if mock.Exists("/test/old.txt") {
		t.Error("old file should not exist after rename")
	}

	if !mock.Exists("/test/new.txt") {
		t.Error("new file should exist after rename")
	}

	data, _ := mock.ReadFile("/test/new.txt")
	if string(data) != "content" {
		t.Errorf("expected 'content', got %q", string(data))
	}
}

func TestMockFileSystem_CopyFile(t *testing.T) {
	mock := NewMockFileSystem()

	mock.WriteFile("/test/src.txt", []byte("source content"), 0644)

	err := mock.CopyFile("/test/src.txt", "/test/dst.txt", 0644)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	data, _ := mock.ReadFile("/test/dst.txt")
	if string(data) != "source content" {
		t.Errorf("expected 'source content', got %q", string(data))
	}

	if len(mock.CopiedFiles) != 1 {
		t.Errorf("expected 1 copy record, got %d", len(mock.CopiedFiles))
	}
}

func TestMockFileSystem_Stat(t *testing.T) {
	mock := NewMockFileSystem()

	mock.WriteFile("/test/file.txt", []byte("content"), 0644)
	mock.MkdirAll("/test/dir", 0755)

	// Stat file
	info, err := mock.Stat("/test/file.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if info.IsDir() {
		t.Error("file should not be reported as directory")
	}

	if info.Name() != "file.txt" {
		t.Errorf("expected name 'file.txt', got %q", info.Name())
	}

	// Stat directory
	info, err = mock.Stat("/test/dir")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !info.IsDir() {
		t.Error("directory should be reported as directory")
	}

	// Stat non-existent file
	_, err = mock.Stat("/test/nonexistent")
	if !os.IsNotExist(err) {
		t.Errorf("expected os.ErrNotExist, got %v", err)
	}
}

func TestMockFileSystem_CustomBehavior(t *testing.T) {
	mock := NewMockFileSystem()

	// Override Exists behavior
	mock.ExistsFunc = func(path string) bool {
		return path == "/allowed"
	}

	if !mock.Exists("/allowed") {
		t.Error("should return true for allowed path")
	}

	if mock.Exists("/notallowed") {
		t.Error("should return false for non-allowed path")
	}

	// Override ReadFile behavior
	mock.ReadFileFunc = func(path string) ([]byte, error) {
		if path == "/error" {
			return nil, fmt.Errorf("read error")
		}
		return []byte("custom"), nil
	}

	data, err := mock.ReadFile("/normal")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if string(data) != "custom" {
		t.Errorf("expected 'custom', got %q", string(data))
	}

	_, err = mock.ReadFile("/error")
	if err == nil {
		t.Error("expected error for /error path")
	}
}

func TestMockHTTPClient_RecordsRequests(t *testing.T) {
	mock := NewMockHTTPClient()

	req, _ := http.NewRequest("GET", "http://example.com/file.zip", nil)
	resp, err := mock.Do(req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200, got %d", resp.StatusCode)
	}

	requests := mock.GetRequests()
	if len(requests) != 1 {
		t.Errorf("expected 1 request, got %d", len(requests))
	}

	if requests[0].URL.String() != "http://example.com/file.zip" {
		t.Errorf("unexpected URL: %s", requests[0].URL.String())
	}
}

func TestMockHTTPClient_CustomBehavior(t *testing.T) {
	mock := NewMockHTTPClient()
	mock.DoFunc = func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusNotFound,
			Body:       http.NoBody,
		}, nil
	}

	req, _ := http.NewRequest("GET", "http://example.com/notfound", nil)
	resp, err := mock.Do(req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404, got %d", resp.StatusCode)
	}
}

func TestMockHTTPClient_Reset(t *testing.T) {
	mock := NewMockHTTPClient()

	req, _ := http.NewRequest("GET", "http://example.com/", nil)
	mock.Do(req)
	mock.Do(req)

	mock.Reset()

	requests := mock.GetRequests()
	if len(requests) != 0 {
		t.Errorf("expected 0 requests after reset, got %d", len(requests))
	}
}

func TestMockDownloader_RecordsDownloads(t *testing.T) {
	mock := NewMockDownloader()

	err := mock.DownloadFile(context.Background(), "http://example.com/file.zip", "/tmp/file.zip", nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(mock.Downloads) != 1 {
		t.Fatalf("expected 1 download, got %d", len(mock.Downloads))
	}

	if mock.Downloads[0].URL != "http://example.com/file.zip" {
		t.Errorf("unexpected URL: %s", mock.Downloads[0].URL)
	}

	if mock.Downloads[0].Dest != "/tmp/file.zip" {
		t.Errorf("unexpected destination: %s", mock.Downloads[0].Dest)
	}
}

func TestMockDownloader_CustomBehavior(t *testing.T) {
	mock := NewMockDownloader()
	mock.Func = func(ctx context.Context, url, dest string, progress func(int64, int64)) error {
		return fmt.Errorf("network error")
	}

	err := mock.DownloadFile(context.Background(), "http://example.com/file.zip", "/tmp/file.zip", nil)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if err.Error() != "network error" {
		t.Errorf("expected 'network error', got %q", err.Error())
	}
}

func TestMockOutputWriter_RecordsOutput(t *testing.T) {
	mock := NewMockOutputWriter()

	mock.WriteOutput("Installing MPV...")
	mock.WriteOutput("Download complete")

	outputs := mock.GetOutputs()
	if len(outputs) != 2 {
		t.Fatalf("expected 2 outputs, got %d", len(outputs))
	}

	if outputs[0] != "Installing MPV..." {
		t.Errorf("unexpected output[0]: %q", outputs[0])
	}

	if outputs[1] != "Download complete" {
		t.Errorf("unexpected output[1]: %q", outputs[1])
	}
}

func TestMockOutputWriter_RecordsErrors(t *testing.T) {
	mock := NewMockOutputWriter()

	mock.WriteError(fmt.Errorf("error 1"))
	mock.WriteError(fmt.Errorf("error 2"))

	errors := mock.GetErrors()
	if len(errors) != 2 {
		t.Fatalf("expected 2 errors, got %d", len(errors))
	}
}

func TestMockOutputWriter_Reset(t *testing.T) {
	mock := NewMockOutputWriter()

	mock.WriteOutput("test")
	mock.WriteError(fmt.Errorf("test"))

	mock.Reset()

	if len(mock.GetOutputs()) != 0 {
		t.Error("outputs should be empty after reset")
	}

	if len(mock.GetErrors()) != 0 {
		t.Error("errors should be empty after reset")
	}
}

func TestMockArchiveExtractor_RecordsExtractions(t *testing.T) {
	mock := NewMockArchiveExtractor()

	err := mock.Extract(context.Background(), "/tmp/archive.zip", "/tmp/extracted")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(mock.Extractions) != 1 {
		t.Fatalf("expected 1 extraction, got %d", len(mock.Extractions))
	}

	if mock.Extractions[0].Src != "/tmp/archive.zip" {
		t.Errorf("unexpected src: %s", mock.Extractions[0].Src)
	}

	if mock.Extractions[0].Dest != "/tmp/extracted" {
		t.Errorf("unexpected dest: %s", mock.Extractions[0].Dest)
	}
}

func TestMockArchiveExtractor_SupportedFormats(t *testing.T) {
	mock := NewMockArchiveExtractor()

	formats := mock.SupportedFormats()
	if len(formats) == 0 {
		t.Error("expected at least one supported format")
	}

	found := false
	for _, f := range formats {
		if f == ".zip" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected .zip to be in supported formats")
	}
}

func TestMockArchiveExtractor_CustomBehavior(t *testing.T) {
	mock := NewMockArchiveExtractor()
	mock.Func = func(ctx context.Context, src, dest string) error {
		return fmt.Errorf("extraction failed")
	}

	err := mock.Extract(context.Background(), "/tmp/archive.zip", "/tmp/extracted")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if err.Error() != "extraction failed" {
		t.Errorf("expected 'extraction failed', got %q", err.Error())
	}
}

// Test compile-time interface compliance
func TestInterfaceCompliance(t *testing.T) {
	// These compile-time checks are in the source files, but we can
	// verify the interfaces are satisfied here as well

	var _ CommandExecutor = NewMockCommandExecutor()
	var _ FileSystem = NewMockFileSystem()
	var _ HTTPClient = NewMockHTTPClient()
	var _ Downloader = NewMockDownloader()
	var _ OutputWriter = NewMockOutputWriter()
	var _ ArchiveExtractor = NewMockArchiveExtractor()
}
