package file

import (
	"context"
	"fmt"
	"io"
	"os"
	"path"

	"github.com/bmatcuk/doublestar/v4"

	"github.com/anchore/stereoscope/pkg/file"
)

var _ Resolver = (*MockResolver)(nil)
var _ OCIMediaTypeResolver = (*MockResolver)(nil)

// MockResolver implements the FileResolver interface and is intended for use *only in test code*.
// It provides an implementation that can resolve local filesystem paths using only a provided discrete list of file
// paths, which are typically paths to test fixtures.
type MockResolver struct {
	locations      []Location
	metadata       map[Coordinates]Metadata
	mimeTypeIndex  map[string][]Location
	mediaTypeIndex map[string][]Location
	extension      map[string][]Location
	basename       map[string][]Location
	ociRef         string
}

// NewMockResolverForPaths creates a new MockResolver, where the only resolvable
// files are those specified by the supplied paths.
func NewMockResolverForPaths(paths ...string) *MockResolver {
	var locations []Location
	extension := make(map[string][]Location)
	basename := make(map[string][]Location)
	for _, p := range paths {
		loc := NewLocation(p)
		locations = append(locations, loc)
		ext := path.Ext(p)
		extension[ext] = append(extension[ext], loc)
		bn := path.Base(p)
		basename[bn] = append(basename[bn], loc)
	}

	return &MockResolver{
		locations: locations,
		metadata:  make(map[Coordinates]Metadata),
		extension: extension,
		basename:  basename,
	}
}

func NewMockResolverForPathsWithMetadata(metadata map[Coordinates]Metadata) *MockResolver {
	var locations []Location
	var mimeTypeIndex = make(map[string][]Location)
	extension := make(map[string][]Location)
	basename := make(map[string][]Location)
	for c, m := range metadata {
		l := NewLocationFromCoordinates(c)
		locations = append(locations, l)
		mimeTypeIndex[m.MIMEType] = append(mimeTypeIndex[m.MIMEType], l)
		ext := path.Ext(l.RealPath)
		extension[ext] = append(extension[ext], l)
		bn := path.Base(l.RealPath)
		basename[bn] = append(basename[bn], l)
	}

	return &MockResolver{
		locations:     locations,
		metadata:      metadata,
		mimeTypeIndex: mimeTypeIndex,
		extension:     extension,
		basename:      basename,
	}
}

// NewMockResolverForOCIArtifact creates a MockResolver that can resolve files
// by media type AND surfaces the given OCI ref via the ImageReference method.
// Intended for tests that exercise the catalogers' OCI-artifact-aware naming
// code paths.
func NewMockResolverForOCIArtifact(ref string, mediaTypes map[string][]Location) *MockResolver {
	r := NewMockResolverForMediaTypes(mediaTypes)
	r.ociRef = ref
	return r
}

// NewMockResolverForMediaTypes creates a MockResolver that can resolve files by media type.
// The mediaTypes map specifies which locations should be returned for each media type.
func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolver {
	var locations []Location
	mediaTypeIndex := make(map[string][]Location)
	extension := make(map[string][]Location)
	basename := make(map[string][]Location)

	for mediaType, locs := range mediaTypes {
		mediaTypeIndex[mediaType] = append(mediaTypeIndex[mediaType], locs...)
		for _, l := range locs {
			locations = append(locations, l)
			ext := path.Ext(l.RealPath)
			extension[ext] = append(extension[ext], l)
			bn := path.Base(l.RealPath)
			basename[bn] = append(basename[bn], l)
		}
	}

	return &MockResolver{
		locations:      locations,
		metadata:       make(map[Coordinates]Metadata),
		mediaTypeIndex: mediaTypeIndex,
		extension:      extension,
		basename:       basename,
	}
}

// ImageReference returns the image reference associated with this mock, if any.
func (r MockResolver) ImageReference() string {
	return r.ociRef
}

// HasPath indicates if the given path exists in the underlying source.
func (r MockResolver) HasPath(path string) bool {
	for _, l := range r.locations {
		if l.RealPath == path {
			return true
		}
	}
	return false
}

// String returns the string representation of the MockResolver.
func (r MockResolver) String() string {
	return fmt.Sprintf("mock:(%s,...)", r.locations[0].RealPath)
}

// FileContentsByLocation fetches file contents for a single location. If the
// path does not exist, an error is returned.
func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
	for _, l := range r.locations {
		if l.Coordinates == location.Coordinates {
			return os.Open(location.RealPath)
		}
	}

	return nil, fmt.Errorf("no file for location: %v", location)
}

// FilesByPath returns all Locations that match the given paths.
func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) {
	var results []Location
	for _, p := range paths {
		for _, location := range r.locations {
			if p == location.RealPath {
				results = append(results, NewLocation(p))
			}
		}
	}

	return results, nil
}

// FilesByGlob returns all Locations that match the given path glob pattern.
func (r MockResolver) FilesByGlob(patterns ...string) ([]Location, error) {
	var results []Location
	for _, pattern := range patterns {
		for _, location := range r.locations {
			matches, err := doublestar.Match(pattern, location.RealPath)
			if err != nil {
				return nil, err
			}
			if matches {
				results = append(results, location)
			}
		}
	}

	return results, nil
}

// RelativeFileByPath returns a single Location for the given path.
func (r MockResolver) RelativeFileByPath(_ Location, path string) *Location {
	paths, err := r.FilesByPath(path)
	if err != nil {
		return nil
	}

	if len(paths) < 1 {
		return nil
	}

	return &paths[0]
}

func (r MockResolver) AllLocations(ctx context.Context) <-chan Location {
	results := make(chan Location)
	go func() {
		defer close(results)
		for _, l := range r.locations {
			select {
			case <-ctx.Done():
				return
			case results <- l:
				continue
			}
		}
	}()
	return results
}

func (r MockResolver) FileMetadataByLocation(l Location) (Metadata, error) {
	info, err := os.Stat(l.RealPath)
	if err != nil {
		return Metadata{}, err
	}

	// other types not supported
	ty := file.TypeRegular
	if info.IsDir() {
		ty = file.TypeDirectory
	}

	return Metadata{
		FileInfo: info,
		Type:     ty,
		UserID:   0, // not supported
		GroupID:  0, // not supported
	}, nil
}

func (r MockResolver) FilesByMIMEType(types ...string) ([]Location, error) {
	var locations []Location
	for _, ty := range types {
		locations = append(r.mimeTypeIndex[ty], locations...)
	}
	return locations, nil
}

func (r MockResolver) FilesByMediaType(types ...string) ([]Location, error) {
	var locations []Location
	for _, ty := range types {
		locations = append(locations, r.mediaTypeIndex[ty]...)
	}
	return locations, nil
}

func (r MockResolver) FilesByExtension(extensions ...string) ([]Location, error) {
	var results []Location
	for _, ext := range extensions {
		results = append(results, r.extension[ext]...)
	}
	return results, nil
}

func (r MockResolver) FilesByBasename(filenames ...string) ([]Location, error) {
	var results []Location
	for _, filename := range filenames {
		results = append(results, r.basename[filename]...)
	}
	return results, nil
}

func (r MockResolver) FilesByBasenameGlob(_ ...string) ([]Location, error) {
	// TODO implement me
	panic("implement me")
}

func (r MockResolver) Write(_ Location, _ io.Reader) error {
	return nil
}
