package tsbaseline

import (
	"slices"
	"strings"
	"testing"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnosticwriter"
	"github.com/microsoft/typescript-go/internal/parser"
	"github.com/microsoft/typescript-go/internal/testutil/baseline"
	"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

func DoJSEmitBaseline(
	t *testing.T,
	baselinePath string,
	header string,
	options *core.CompilerOptions,
	result *harnessutil.CompilationResult,
	tsConfigFiles []*harnessutil.TestFile,
	toBeCompiled []*harnessutil.TestFile,
	otherFiles []*harnessutil.TestFile,
	harnessSettings *harnessutil.HarnessOptions,
	opts baseline.Options,
) {
	if !options.NoEmit.IsTrue() && !options.EmitDeclarationOnly.IsTrue() && result.JS.Size() == 0 && len(result.Diagnostics) == 0 {
		t.Fatal("Expected at least one js file to be emitted or at least one error to be created.")
	}

	// check js output
	var tsCode strings.Builder
	tsSources := core.Concatenate(otherFiles, toBeCompiled)
	tsCode.WriteString("//// [")
	tsCode.WriteString(header)
	tsCode.WriteString("] ////\r\n\r\n")

	for i, file := range tsSources {
		tsCode.WriteString("//// [")
		tsCode.WriteString(tspath.GetBaseFileName(file.UnitName))
		tsCode.WriteString("]\r\n")
		tsCode.WriteString(file.Content)
		if i < len(tsSources)-1 {
			tsCode.WriteString("\r\n")
		}
	}

	var jsCode strings.Builder
	for file := range result.JS.Values() {
		if jsCode.Len() > 0 && !strings.HasSuffix(jsCode.String(), "\n") {
			jsCode.WriteString("\r\n")
		}
		if len(result.Diagnostics) == 0 && strings.HasSuffix(file.UnitName, tspath.ExtensionJson) {
			fileParseResult := parser.ParseSourceFile(ast.SourceFileParseOptions{
				FileName: file.UnitName,
				Path:     tspath.Path(file.UnitName),
			}, file.Content, core.ScriptKindJSON)
			if len(fileParseResult.Diagnostics()) > 0 {
				jsCode.WriteString(GetErrorBaseline(t, []*harnessutil.TestFile{file}, diagnosticwriter.WrapASTDiagnostics(fileParseResult.Diagnostics()), diagnosticwriter.CompareASTDiagnostics, false /*pretty*/))
				continue
			}
		}
		jsCode.WriteString(fileOutput(file, harnessSettings))
	}

	if result.DTS.Size() > 0 {
		jsCode.WriteString("\r\n\r\n")
		for declFile := range result.DTS.Values() {
			jsCode.WriteString(fileOutput(declFile, harnessSettings))
		}
	}

	declFileContext := prepareDeclarationCompilationContext(
		toBeCompiled,
		otherFiles,
		result,
		harnessSettings,
		options,
		"", /*currentDirectory*/
	)
	declFileCompilationResult := compileDeclarationFiles(t, declFileContext, result.Symlinks)

	if declFileCompilationResult != nil && len(declFileCompilationResult.declResult.Diagnostics) > 0 {
		jsCode.WriteString("\r\n\r\n//// [DtsFileErrors]\r\n")
		jsCode.WriteString("\r\n\r\n")
		jsCode.WriteString(GetErrorBaseline(
			t,
			slices.Concat(tsConfigFiles, declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles),
			diagnosticwriter.WrapASTDiagnostics(declFileCompilationResult.declResult.Diagnostics),
			diagnosticwriter.CompareASTDiagnostics,
			false, /*pretty*/
		))
	}

	if !options.NoCheck.IsTrue() && !options.NoEmit.IsTrue() {
		testConfig := make(map[string]string)
		testConfig["noCheck"] = "true"
		withoutChecking := result.Repeat(testConfig)
		compareResultFileSets := func(a *collections.OrderedMap[string, *harnessutil.TestFile], b *collections.OrderedMap[string, *harnessutil.TestFile]) {
			for key, doc := range a.Entries() {
				original := b.GetOrZero(key)
				if original == nil {
					jsCode.WriteString("\r\n\r\n!!!! File ")
					jsCode.WriteString(removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/))
					jsCode.WriteString(" missing from original emit, but present in noCheck emit\r\n")
					jsCode.WriteString(fileOutput(doc, harnessSettings))
				} else if original.Content != doc.Content {
					jsCode.WriteString("\r\n\r\n!!!! File ")
					jsCode.WriteString(removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/))
					jsCode.WriteString(" differs from original emit in noCheck emit\r\n")
					var fileName string
					if harnessSettings.FullEmitPaths {
						fileName = removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/)
					} else {
						fileName = tspath.GetBaseFileName(doc.UnitName)
					}
					jsCode.WriteString("//// [")
					jsCode.WriteString(fileName)
					jsCode.WriteString("]\r\n")
					expected := original.Content
					actual := doc.Content
					jsCode.WriteString(baseline.DiffText("Expected\tThe full check baseline", "Actual\twith noCheck set", expected, actual))
				}
			}
		}
		compareResultFileSets(&withoutChecking.DTS, &result.DTS)
		compareResultFileSets(&withoutChecking.JS, &result.JS)
	}

	if tspath.FileExtensionIsOneOf(baselinePath, []string{tspath.ExtensionTs, tspath.ExtensionTsx}) {
		baselinePath = tspath.ChangeExtension(baselinePath, tspath.ExtensionJs)
	}

	var actual string
	if jsCode.Len() > 0 {
		actual = tsCode.String() + "\r\n\r\n" + jsCode.String()
	} else {
		actual = baseline.NoContent
	}

	baseline.Run(t, baselinePath, actual, opts)
}

func fileOutput(file *harnessutil.TestFile, settings *harnessutil.HarnessOptions) string {
	var fileName string
	if settings.FullEmitPaths {
		fileName = removeTestPathPrefixes(file.UnitName, false /*retainTrailingDirectorySeparator*/)
	} else {
		fileName = tspath.GetBaseFileName(file.UnitName)
	}
	return "//// [" + fileName + "]\r\n" + file.Content
}

type declarationCompilationContext struct {
	declInputFiles   []*harnessutil.TestFile
	declOtherFiles   []*harnessutil.TestFile
	harnessSettings  *harnessutil.HarnessOptions
	options          *core.CompilerOptions
	currentDirectory string
	configFile       *tsoptions.TsConfigSourceFile
}

func prepareDeclarationCompilationContext(
	inputFiles []*harnessutil.TestFile,
	otherFiles []*harnessutil.TestFile,
	result *harnessutil.CompilationResult,
	harnessSettings *harnessutil.HarnessOptions,
	options *core.CompilerOptions,
	// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
	currentDirectory string,
) *declarationCompilationContext {
	if options.Declaration.IsTrue() && len(result.Diagnostics) == 0 {
		if options.EmitDeclarationOnly.IsTrue() {
			if result.JS.Size() > 0 {
				panic("Only declaration files should be generated when emitDeclarationOnly:true")
			}
			if result.DTS.Size() == 0 && !options.NoEmit.IsTrue() {
				panic("Expected at least one declaration file to be emitted when emitDeclarationOnly:true and no errors were generated")
			}
		} else if result.DTS.Size() != result.GetNumberOfJSFiles(false /*includeJson*/) {
			panic("There were no errors and declFiles generated did not match number of js files generated")
		}
	}

	var declInputFiles []*harnessutil.TestFile
	var declOtherFiles []*harnessutil.TestFile

	findUnit := func(fileName string, units []*harnessutil.TestFile) *harnessutil.TestFile {
		for _, unit := range units {
			if unit.UnitName == fileName {
				return unit
			}
		}
		return nil
	}

	findResultCodeFile := func(fileName string) *harnessutil.TestFile {
		sourceFile := result.Program.GetSourceFile(fileName)
		if sourceFile == nil {
			panic("Program has no source file with name '" + fileName + "'")
		}
		// Is this file going to be emitted separately
		var sourceFileName string

		if len(options.OutDir) != 0 {
			sourceFilePath := tspath.GetNormalizedAbsolutePath(sourceFile.FileName(), result.Host.GetCurrentDirectory())
			sourceFilePath = strings.Replace(sourceFilePath, result.Program.CommonSourceDirectory(), "", 1)
			sourceFileName = tspath.CombinePaths(options.OutDir, sourceFilePath)
		} else {
			sourceFileName = sourceFile.FileName()
		}

		dTsFileName := tspath.RemoveFileExtension(sourceFileName) + tspath.GetDeclarationEmitExtensionForPath(sourceFileName)
		return result.DTS.GetOrZero(dTsFileName)
	}

	addDtsFile := func(file *harnessutil.TestFile, dtsFiles []*harnessutil.TestFile) []*harnessutil.TestFile {
		if tspath.IsDeclarationFileName(file.UnitName) || tspath.HasJSONFileExtension(file.UnitName) {
			dtsFiles = append(dtsFiles, file)
		} else if tspath.HasTSFileExtension(file.UnitName) || (tspath.HasJSFileExtension(file.UnitName) && options.GetAllowJS()) {
			declFile := findResultCodeFile(file.UnitName)
			if declFile != nil && findUnit(declFile.UnitName, declInputFiles) == nil && findUnit(declFile.UnitName, declOtherFiles) == nil {
				dtsFiles = append(dtsFiles, &harnessutil.TestFile{
					UnitName: declFile.UnitName,
					Content:  strings.TrimPrefix(declFile.Content, "\uFEFF"),
				})
			}
		}
		return dtsFiles
	}

	// if the .d.ts is non-empty, confirm it compiles correctly as well
	if options.Declaration.IsTrue() && len(result.Diagnostics) == 0 && result.DTS.Size() > 0 {
		for _, file := range inputFiles {
			declInputFiles = addDtsFile(file, declInputFiles)
		}
		for _, file := range otherFiles {
			declOtherFiles = addDtsFile(file, declOtherFiles)
		}
		return &declarationCompilationContext{
			declInputFiles:   declInputFiles,
			declOtherFiles:   declOtherFiles,
			harnessSettings:  harnessSettings,
			options:          options,
			currentDirectory: core.IfElse(len(currentDirectory) > 0, currentDirectory, harnessSettings.CurrentDirectory),
			configFile:       result.Program.Program().CommandLine().ConfigFile,
		}
	}
	return nil
}

type declarationCompilationResult struct {
	declInputFiles []*harnessutil.TestFile
	declOtherFiles []*harnessutil.TestFile
	declResult     *harnessutil.CompilationResult
}

func compileDeclarationFiles(t *testing.T, context *declarationCompilationContext, symlinks map[string]string) *declarationCompilationResult {
	if context == nil {
		return nil
	}
	var tsconfig *tsoptions.ParsedCommandLine
	if context.configFile != nil {
		tsconfig = &tsoptions.ParsedCommandLine{
			ConfigFile: context.configFile,
		}
	}
	declFileCompilationResult := harnessutil.CompileFilesEx(t,
		context.declInputFiles,
		context.declOtherFiles,
		context.harnessSettings,
		context.options,
		context.currentDirectory,
		symlinks,
		tsconfig)
	return &declarationCompilationResult{
		context.declInputFiles,
		context.declOtherFiles,
		declFileCompilationResult,
	}
}
