package ui

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"testing"

	"github.com/safedep/dry/usefulerror"
	"github.com/safedep/pmg/errcodes"
	"github.com/safedep/pmg/internal/shim"
	"github.com/stretchr/testify/assert"
)

func Test_ErrorConverters(t *testing.T) {
	tests := []struct {
		name           string
		inputError     error
		wantCode       string
		wantHumanError string
		wantContains   string
		wantNil        bool
	}{
		{
			name: "AlreadyUseful",
			inputError: usefulerror.NewUsefulError().
				WithCode("CUSTOM").
				WithHumanError("Already useful").
				WithMsg("test"),
			wantCode:       "CUSTOM",
			wantHumanError: "Already useful",
		},
		{
			name:         "PackageManagerNotFound",
			inputError:   &shim.BinaryNotFoundError{Name: "bun"},
			wantCode:     errcodes.PackageManagerNotFound,
			wantHumanError: "bun is not installed",
		},
		{
			name:         "FileNotExist",
			inputError:   &fs.PathError{Op: "open", Path: "/nonexistent/file.txt", Err: os.ErrNotExist},
			wantCode:     errcodes.NotFound,
			wantContains: "/nonexistent/file.txt",
		},
		{
			name:         "PermissionDenied",
			inputError:   &fs.PathError{Op: "open", Path: "/root/secret", Err: os.ErrPermission},
			wantCode:     errcodes.PermissionDenied,
			wantContains: "/root/secret",
		},
		{
			name:         "ContextTimeout",
			inputError:   context.DeadlineExceeded,
			wantCode:     errcodes.Timeout,
			wantContains: "timed out",
		},
		{
			name:         "ContextCanceled",
			inputError:   context.Canceled,
			wantCode:     errcodes.Canceled,
			wantContains: "canceled",
		},
		{
			name:       "UnexpectedEOF",
			inputError: io.ErrUnexpectedEOF,
			wantCode:   errcodes.UnexpectedEOF,
		},
		{
			name:       "WrappedError",
			inputError: fmt.Errorf("failed to read config: %w", os.ErrNotExist),
			wantCode:   errcodes.NotFound,
		},
		{
			name:       "UnknownError",
			inputError: errors.New("some unknown error"),
			wantNil:    true,
		},
		{
			name: "UnknownWrappedError",
			inputError: fmt.Errorf("more context: %w",
				fmt.Errorf("outer context: %w",
					errors.New("root cause error"))),
			wantNil: true,
		},
		{
			name:       "Nil",
			inputError: nil,
			wantNil:    true,
		},
		{
			name:       "NetworkErrorMessage",
			inputError: errors.New("connection refused"),
			wantCode:   errcodes.Network,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, ok := usefulerror.AsUsefulError(tt.inputError)

			if tt.wantNil {
				assert.False(t, ok)
				return
			}

			assert.True(t, ok)
			assert.NotNil(t, result)
			assert.Equal(t, tt.wantCode, result.Code())

			if tt.wantHumanError != "" {
				assert.Equal(t, tt.wantHumanError, result.HumanError())
			}

			if tt.wantContains != "" {
				assert.Contains(t, result.HumanError(), tt.wantContains)
			}
		})
	}
}

func Test_convertToUsefulError(t *testing.T) {
	tests := []struct {
		name           string
		inputError     error
		wantCode       string
		wantHumanError string
		wantNil        bool
	}{
		{
			name:       "Nil",
			inputError: nil,
			wantNil:    true,
		},
		{
			name: "AlreadyUseful",
			inputError: usefulerror.NewUsefulError().
				WithCode("CUSTOM").
				WithHumanError("Already useful"),
			wantCode:       "CUSTOM",
			wantHumanError: "Already useful",
		},
		{
			name:           "Converted",
			inputError:     &fs.PathError{Op: "open", Path: "/nonexistent/file.txt", Err: os.ErrNotExist},
			wantCode:       errcodes.NotFound,
			wantHumanError: "File or directory not found: /nonexistent/file.txt",
		},
		{
			name:           "UnknownFallsBackToRootCause",
			inputError:     errors.New("some unknown error"),
			wantCode:       errcodes.Unknown,
			wantHumanError: "some unknown error",
		},
		{
			name: "UnknownWrappedExtractsRootCause",
			inputError: fmt.Errorf("more context: %w",
				fmt.Errorf("outer context: %w",
					errors.New("root cause error"))),
			wantCode:       errcodes.Unknown,
			wantHumanError: "root cause error",
		},
	}

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

			if tt.wantNil {
				assert.Nil(t, result)
				return
			}

			assert.NotNil(t, result)
			assert.Equal(t, tt.wantCode, result.Code())
			assert.Equal(t, tt.wantHumanError, result.HumanError())
		})
	}
}

func TestExtractPathFromError(t *testing.T) {
	tests := []struct {
		name     string
		inputErr error
		wantPath string
	}{
		{
			name:     "PathError",
			inputErr: &fs.PathError{Op: "open", Path: "/some/path", Err: os.ErrNotExist},
			wantPath: "/some/path",
		},
		{
			name:     "LinkError",
			inputErr: &os.LinkError{Op: "link", Old: "/old/path", New: "/new/path", Err: os.ErrPermission},
			wantPath: "/old/path",
		},
		{
			name:     "generic error",
			inputErr: errors.New("some error"),
			wantPath: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			path := extractPathFromError(tt.inputErr)
			assert.Equal(t, tt.wantPath, path)
		})
	}
}
