package installer

import (
	"context"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

// MockCommandExecutor implements CommandExecutor for testing
type MockCommandExecutor struct {
	mu             sync.Mutex
	Commands       []CommandCall
	ShellCommands  []string
	RunCommandFunc func(name string, args ...string) error
	RunShellFunc   func(script string) error
	ctx            context.Context
}

// CommandCall records a single command invocation
type CommandCall struct {
	Name string
	Args []string
}

// NewMockCommandExecutor creates a new mock command executor
func NewMockCommandExecutor() *MockCommandExecutor {
	return &MockCommandExecutor{
		ctx: context.Background(),
	}
}

// RunCommand records and optionally executes a command
func (m *MockCommandExecutor) RunCommand(name string, args ...string) error {
	m.mu.Lock()
	m.Commands = append(m.Commands, CommandCall{Name: name, Args: args})
	m.mu.Unlock()

	if m.RunCommandFunc != nil {
		return m.RunCommandFunc(name, args...)
	}
	return nil
}

// RunShellCommand records and optionally executes a shell command
func (m *MockCommandExecutor) RunShellCommand(script string) error {
	m.mu.Lock()
	m.ShellCommands = append(m.ShellCommands, script)
	m.mu.Unlock()

	if m.RunShellFunc != nil {
		return m.RunShellFunc(script)
	}
	return nil
}

// RunCommands executes multiple shell commands
func (m *MockCommandExecutor) RunCommands(commands []string) error {
	for _, cmd := range commands {
		if err := m.RunShellCommand(cmd); err != nil {
			return err
		}
	}
	return nil
}

// Context returns the command executor's context
func (m *MockCommandExecutor) Context() context.Context {
	return m.ctx
}

// SetContext sets the context for the command executor
func (m *MockCommandExecutor) SetContext(ctx context.Context) {
	m.ctx = ctx
}

// IsCancelled returns true if the context has been cancelled
func (m *MockCommandExecutor) IsCancelled() bool {
	select {
	case <-m.ctx.Done():
		return true
	default:
		return false
	}
}

// CheckCancelled returns an error if the context has been cancelled
func (m *MockCommandExecutor) CheckCancelled() error {
	if m.IsCancelled() {
		return m.ctx.Err()
	}
	return nil
}

// GetCommands returns a copy of all recorded commands
func (m *MockCommandExecutor) GetCommands() []CommandCall {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]CommandCall, len(m.Commands))
	copy(result, m.Commands)
	return result
}

// GetShellCommands returns a copy of all recorded shell commands
func (m *MockCommandExecutor) GetShellCommands() []string {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]string, len(m.ShellCommands))
	copy(result, m.ShellCommands)
	return result
}

// Reset clears all recorded commands
func (m *MockCommandExecutor) Reset() {
	m.mu.Lock()
	m.Commands = nil
	m.ShellCommands = nil
	m.mu.Unlock()
}

// MockFileSystem implements FileSystem for testing
type MockFileSystem struct {
	mu           sync.Mutex
	Files        map[string][]byte
	Dirs         map[string]bool
	CopiedFiles  []CopyCall
	RenamedFiles []RenameCall

	// Function overrides for custom behavior
	ExistsFunc    func(path string) bool
	ReadFileFunc  func(path string) ([]byte, error)
	WriteFileFunc func(path string, data []byte, perm os.FileMode) error
	MkdirAllFunc  func(path string, perm os.FileMode) error
	RemoveFunc    func(path string) error
	StatFunc      func(path string) (os.FileInfo, error)
}

// CopyCall records a file copy operation
type CopyCall struct {
	Src string
	Dst string
}

// RenameCall records a file rename operation
type RenameCall struct {
	OldPath string
	NewPath string
}

// NewMockFileSystem creates a new mock file system
func NewMockFileSystem() *MockFileSystem {
	return &MockFileSystem{
		Files: make(map[string][]byte),
		Dirs:  make(map[string]bool),
	}
}

// Exists checks if a path exists
func (m *MockFileSystem) Exists(path string) bool {
	if m.ExistsFunc != nil {
		return m.ExistsFunc(path)
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	_, hasFile := m.Files[path]
	_, hasDir := m.Dirs[path]
	return hasFile || hasDir
}

// IsDir checks if a path is a directory
func (m *MockFileSystem) IsDir(path string) bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.Dirs[path]
}

// MkdirAll creates a directory tree
func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error {
	if m.MkdirAllFunc != nil {
		return m.MkdirAllFunc(path, perm)
	}
	m.mu.Lock()
	m.Dirs[path] = true
	m.mu.Unlock()
	return nil
}

// MkdirTemp creates a temporary directory
func (m *MockFileSystem) MkdirTemp(dir, prefix string) (string, error) {
	return "/tmp/mocktemp_" + prefix, nil
}

// ReadFile reads file contents
func (m *MockFileSystem) ReadFile(path string) ([]byte, error) {
	if m.ReadFileFunc != nil {
		return m.ReadFileFunc(path)
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	data, ok := m.Files[path]
	if !ok {
		return nil, os.ErrNotExist
	}
	return data, nil
}

// WriteFile writes data to a file
func (m *MockFileSystem) WriteFile(path string, data []byte, perm os.FileMode) error {
	if m.WriteFileFunc != nil {
		return m.WriteFileFunc(path, data, perm)
	}
	m.mu.Lock()
	m.Files[path] = data
	m.mu.Unlock()
	return nil
}

// Remove removes a file or directory
func (m *MockFileSystem) Remove(path string) error {
	if m.RemoveFunc != nil {
		return m.RemoveFunc(path)
	}
	m.mu.Lock()
	delete(m.Files, path)
	delete(m.Dirs, path)
	m.mu.Unlock()
	return nil
}

// RemoveAll removes a path and all children
func (m *MockFileSystem) RemoveAll(path string) error {
	m.mu.Lock()
	for p := range m.Files {
		if len(p) >= len(path) && p[:len(path)] == path {
			delete(m.Files, p)
		}
	}
	for p := range m.Dirs {
		if len(p) >= len(path) && p[:len(path)] == path {
			delete(m.Dirs, p)
		}
	}
	m.mu.Unlock()
	return nil
}

// Rename renames a file or directory
func (m *MockFileSystem) Rename(oldPath, newPath string) error {
	m.mu.Lock()
	m.RenamedFiles = append(m.RenamedFiles, RenameCall{OldPath: oldPath, NewPath: newPath})
	if data, ok := m.Files[oldPath]; ok {
		m.Files[newPath] = data
		delete(m.Files, oldPath)
	}
	m.mu.Unlock()
	return nil
}

// CopyFile copies a file
func (m *MockFileSystem) CopyFile(src, dst string, perm os.FileMode) error {
	m.mu.Lock()
	m.CopiedFiles = append(m.CopiedFiles, CopyCall{Src: src, Dst: dst})
	if data, ok := m.Files[src]; ok {
		m.Files[dst] = data
	}
	m.mu.Unlock()
	return nil
}

// CopyDir copies a directory
func (m *MockFileSystem) CopyDir(src, dst string) error {
	m.mu.Lock()
	for path, data := range m.Files {
		if len(path) >= len(src) && path[:len(src)] == src {
			newPath := dst + path[len(src):]
			m.Files[newPath] = data
		}
	}
	m.mu.Unlock()
	return nil
}

// Stat returns file info for a path
func (m *MockFileSystem) Stat(path string) (os.FileInfo, error) {
	if m.StatFunc != nil {
		return m.StatFunc(path)
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	if _, ok := m.Files[path]; ok {
		return mockFileInfo{name: filepath.Base(path), isDir: false}, nil
	}
	if ok := m.Dirs[path]; ok {
		return mockFileInfo{name: filepath.Base(path), isDir: true}, nil
	}
	return nil, os.ErrNotExist
}

// Walk walks a directory tree
func (m *MockFileSystem) Walk(root string, walkFn filepath.WalkFunc) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	for path := range m.Files {
		if len(path) >= len(root) && path[:len(root)] == root {
			walkFn(path, mockFileInfo{name: filepath.Base(path)}, nil)
		}
	}
	return nil
}

// Glob returns matches for a pattern
func (m *MockFileSystem) Glob(pattern string) ([]string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var matches []string
	for path := range m.Files {
		matched, _ := filepath.Match(pattern, path)
		if matched {
			matches = append(matches, path)
		}
	}
	return matches, nil
}

// mockFileInfo implements os.FileInfo for testing
type mockFileInfo struct {
	name  string
	isDir bool
}

func (m mockFileInfo) Name() string       { return m.name }
func (m mockFileInfo) Size() int64        { return 0 }
func (m mockFileInfo) Mode() os.FileMode  { return 0644 }
func (m mockFileInfo) ModTime() time.Time { return time.Time{} }
func (m mockFileInfo) IsDir() bool        { return m.isDir }
func (m mockFileInfo) Sys() interface{}   { return nil }

// MockHTTPClient implements HTTPClient for testing
type MockHTTPClient struct {
	mu       sync.Mutex
	Requests []*http.Request
	DoFunc   func(req *http.Request) (*http.Response, error)
}

// NewMockHTTPClient creates a new mock HTTP client
func NewMockHTTPClient() *MockHTTPClient {
	return &MockHTTPClient{}
}

// Do records and optionally executes an HTTP request
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
	m.mu.Lock()
	m.Requests = append(m.Requests, req)
	m.mu.Unlock()

	if m.DoFunc != nil {
		return m.DoFunc(req)
	}

	// Default: return 200 OK with empty body
	return &http.Response{
		StatusCode: http.StatusOK,
		Body:       io.NopCloser(strings.NewReader("")),
	}, nil
}

// GetRequests returns a copy of all recorded requests
func (m *MockHTTPClient) GetRequests() []*http.Request {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]*http.Request, len(m.Requests))
	copy(result, m.Requests)
	return result
}

// Reset clears all recorded requests
func (m *MockHTTPClient) Reset() {
	m.mu.Lock()
	m.Requests = nil
	m.mu.Unlock()
}

// MockDownloader implements Downloader for testing
type MockDownloader struct {
	Downloads []DownloadCall
	Func      func(ctx context.Context, url, dest string, progress func(int64, int64)) error
}

// DownloadCall records a download operation
type DownloadCall struct {
	URL  string
	Dest string
}

// NewMockDownloader creates a new mock downloader
func NewMockDownloader() *MockDownloader {
	return &MockDownloader{}
}

// DownloadFile records and optionally performs a download
func (m *MockDownloader) DownloadFile(ctx context.Context, url, dest string, progress func(int64, int64)) error {
	m.Downloads = append(m.Downloads, DownloadCall{URL: url, Dest: dest})
	if m.Func != nil {
		return m.Func(ctx, url, dest, progress)
	}
	return nil
}

// MockOutputWriter implements OutputWriter for testing
type MockOutputWriter struct {
	mu      sync.Mutex
	Outputs []string
	Errors  []error
}

// NewMockOutputWriter creates a new mock output writer
func NewMockOutputWriter() *MockOutputWriter {
	return &MockOutputWriter{}
}

// WriteOutput records an output message
func (m *MockOutputWriter) WriteOutput(msg string) {
	m.mu.Lock()
	m.Outputs = append(m.Outputs, msg)
	m.mu.Unlock()
}

// WriteError records an error
func (m *MockOutputWriter) WriteError(err error) {
	m.mu.Lock()
	m.Errors = append(m.Errors, err)
	m.mu.Unlock()
}

// GetOutputs returns a copy of all recorded outputs
func (m *MockOutputWriter) GetOutputs() []string {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]string, len(m.Outputs))
	copy(result, m.Outputs)
	return result
}

// GetErrors returns a copy of all recorded errors
func (m *MockOutputWriter) GetErrors() []error {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]error, len(m.Errors))
	copy(result, m.Errors)
	return result
}

// Reset clears all recorded outputs and errors
func (m *MockOutputWriter) Reset() {
	m.mu.Lock()
	m.Outputs = nil
	m.Errors = nil
	m.mu.Unlock()
}

// MockArchiveExtractor implements ArchiveExtractor for testing
type MockArchiveExtractor struct {
	Extractions          []ExtractionCall
	Func                 func(ctx context.Context, src, dest string) error
	SupportedFormatsList []string
}

// ExtractionCall records an extraction operation
type ExtractionCall struct {
	Src  string
	Dest string
}

// NewMockArchiveExtractor creates a new mock archive extractor
func NewMockArchiveExtractor() *MockArchiveExtractor {
	return &MockArchiveExtractor{
		SupportedFormatsList: []string{".zip", ".tar.gz", ".tar.xz", ".7z"},
	}
}

// Extract records and optionally performs an extraction
func (m *MockArchiveExtractor) Extract(ctx context.Context, src, dest string) error {
	m.Extractions = append(m.Extractions, ExtractionCall{Src: src, Dest: dest})
	if m.Func != nil {
		return m.Func(ctx, src, dest)
	}
	return nil
}

// SupportedFormats returns the list of supported archive formats
func (m *MockArchiveExtractor) SupportedFormats() []string {
	return m.SupportedFormatsList
}
