package testutils

import (
	"fmt"
	"go/build"
	"io/ioutil"
	"log"
	"os"
	"path"
	"strings"

	"github.com/securego/gosec/v2"
	"golang.org/x/tools/go/packages"
)

type buildObj struct {
	pkg    *build.Package
	config *packages.Config
	pkgs   []*packages.Package
}

// TestPackage is a mock package for testing purposes
type TestPackage struct {
	Path   string
	Files  map[string]string
	onDisk bool
	build  *buildObj
}

// NewTestPackage will create a new and empty package. Must call Close() to cleanup
// auxiliary files
func NewTestPackage() *TestPackage {
	workingDir, err := ioutil.TempDir("", "gosecs_test")
	if err != nil {
		return nil
	}

	return &TestPackage{
		Path:   workingDir,
		Files:  make(map[string]string),
		onDisk: false,
		build:  nil,
	}
}

// AddFile inserts the filename and contents into the package contents
func (p *TestPackage) AddFile(filename, content string) {
	p.Files[path.Join(p.Path, filename)] = content
}

func (p *TestPackage) write() error {
	if p.onDisk {
		return nil
	}
	for filename, content := range p.Files {
		if e := ioutil.WriteFile(filename, []byte(content), 0o644); e != nil {
			return e
		} // #nosec G306
	}
	p.onDisk = true
	return nil
}

// Build ensures all files are persisted to disk and built
func (p *TestPackage) Build() error {
	if p.build != nil {
		return nil
	}
	if err := p.write(); err != nil {
		return err
	}
	basePackage, err := build.Default.ImportDir(p.Path, build.ImportComment)
	if err != nil {
		return err
	}

	var packageFiles []string
	for _, filename := range basePackage.GoFiles {
		packageFiles = append(packageFiles, path.Join(p.Path, filename))
	}

	conf := &packages.Config{
		Mode:  gosec.LoadMode,
		Tests: false,
	}
	pkgs, err := packages.Load(conf, packageFiles...)
	if err != nil {
		return err
	}
	p.build = &buildObj{
		pkg:    basePackage,
		config: conf,
		pkgs:   pkgs,
	}
	return nil
}

// CreateContext builds a context out of supplied package context
func (p *TestPackage) CreateContext(filename string) *gosec.Context {
	if err := p.Build(); err != nil {
		log.Fatal(err)
		return nil
	}

	for _, pkg := range p.build.pkgs {
		for _, file := range pkg.Syntax {
			pkgFile := pkg.Fset.File(file.Pos()).Name()
			strip := fmt.Sprintf("%s%c", p.Path, os.PathSeparator)
			pkgFile = strings.TrimPrefix(pkgFile, strip)
			if pkgFile == filename {
				ctx := &gosec.Context{
					FileSet:      pkg.Fset,
					Root:         file,
					Config:       gosec.NewConfig(),
					Info:         pkg.TypesInfo,
					Pkg:          pkg.Types,
					Imports:      gosec.NewImportTracker(),
					PassedValues: make(map[string]interface{}),
				}
				ctx.Imports.TrackPackages(ctx.Pkg.Imports()...)
				return ctx
			}
		}
	}
	return nil
}

// Close will delete the package and all files in that directory
func (p *TestPackage) Close() {
	if p.onDisk {
		err := os.RemoveAll(p.Path)
		if err != nil {
			log.Fatal(err)
		}
	}
}

// Pkgs returns the current built packages
func (p *TestPackage) Pkgs() []*packages.Package {
	if p.build != nil {
		return p.build.pkgs
	}
	return []*packages.Package{}
}

// PrintErrors prints to os.Stderr the accumulated errors of built packages
func (p *TestPackage) PrintErrors() int {
	return packages.PrintErrors(p.Pkgs())
}
