package tsctests

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/microsoft/typescript-go/internal/execute"
	"github.com/microsoft/typescript-go/internal/execute/tsc"
	"gotest.tools/v3/assert"
)

// createTestWatcher sets up a minimal project with a tsconfig and
// returns a Watcher ready for concurrent testing, plus the TestSys
// for file manipulation.
func createTestWatcher(t *testing.T) (*execute.Watcher, *TestSys) {
	t.Helper()
	input := &tscInput{
		files: FileMap{
			"/home/src/workspaces/project/a.ts":          `const a: number = 1;`,
			"/home/src/workspaces/project/b.ts":          `import { a } from "./a"; export const b = a;`,
			"/home/src/workspaces/project/tsconfig.json": `{}`,
		},
		commandLineArgs: []string{"--watch"},
	}
	sys := newTestSys(input, false)
	result := execute.CommandLine(context.Background(), sys, []string{"--watch"}, sys)
	if result.Watcher == nil {
		t.Fatal("expected Watcher to be non-nil in watch mode")
	}
	w, ok := result.Watcher.(*execute.Watcher)
	if !ok {
		t.Fatalf("expected *execute.Watcher, got %T", result.Watcher)
	}
	return w, sys
}

// TestWatcherConcurrentDoCycle calls DoCycle from multiple goroutines
// while modifying source files, exposing data races on Watcher fields
// such as configModified, program, config, and the underlying
// FileWatcher state. Run with -race to detect.
func TestWatcherConcurrentDoCycle(t *testing.T) {
	t.Parallel()
	w, sys := createTestWatcher(t)

	var wg sync.WaitGroup

	for i := range 8 {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := range 10 {
				_ = sys.fsFromFileMap().WriteFile(
					"/home/src/workspaces/project/a.ts",
					fmt.Sprintf("const a: number = %d;", i*10+j),
				)
				w.DoCycle()
			}
		}(i)
	}

	wg.Wait()
}

// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle from
// multiple goroutines, some modifying files and some not, to test
// concurrent access to all Watcher and FileWatcher state.
func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) {
	t.Parallel()
	w, sys := createTestWatcher(t)

	var wg sync.WaitGroup

	// DoCycle goroutines
	for i := range 4 {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := range 15 {
				_ = sys.fsFromFileMap().WriteFile(
					"/home/src/workspaces/project/a.ts",
					fmt.Sprintf("const a: number = %d;", i*15+j),
				)
				w.DoCycle()
			}
		}(i)
	}

	// State reader goroutines
	for range 8 {
		wg.Go(func() {
			for range 50 {
				w.DoCycle()
				w.DoCycle()
				w.DoCycle()
				w.DoCycle()
			}
		})
	}

	wg.Wait()
}

// TestWatcherConcurrentFileChangesAndDoCycle creates, modifies, and
// deletes files from multiple goroutines while DoCycle runs, testing
// races between FS mutations and watch state updates.
func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) {
	t.Parallel()
	w, sys := createTestWatcher(t)

	var wg sync.WaitGroup

	// File creators
	for i := range 4 {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := range 20 {
				path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j)
				_ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j))
			}
		}(i)
	}

	// File deleters
	wg.Go(func() {
		for j := range 20 {
			_ = sys.fsFromFileMap().Remove(
				fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j),
			)
		}
	})

	// DoCycle callers
	for range 4 {
		wg.Go(func() {
			for range 10 {
				w.DoCycle()
			}
		})
	}

	wg.Wait()
}

// TestWatcherRapidConfigChanges modifies tsconfig.json rapidly from
// multiple goroutines while DoCycle runs, testing races on
// config-related fields (configModified, configHasErrors,
// configFilePaths, config, extendedConfigCache).
func TestWatcherRapidConfigChanges(t *testing.T) {
	t.Parallel()
	w, sys := createTestWatcher(t)

	var wg sync.WaitGroup

	configs := []string{
		`{}`,
		`{"compilerOptions": {"strict": true}}`,
		`{"compilerOptions": {"target": "ES2020"}}`,
		`{"compilerOptions": {"noEmit": true}}`,
	}

	// Config modifiers + DoCycle
	for i := range 3 {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := range 10 {
				_ = sys.fsFromFileMap().WriteFile(
					"/home/src/workspaces/project/tsconfig.json",
					configs[(i+j)%len(configs)],
				)
				w.DoCycle()
			}
		}(i)
	}

	// Concurrent source file modifications
	for i := range 2 {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := range 15 {
				_ = sys.fsFromFileMap().WriteFile(
					"/home/src/workspaces/project/a.ts",
					fmt.Sprintf("const a: number = %d;", i*15+j),
				)
				w.DoCycle()
			}
		}(i)
	}

	// State readers
	for range 4 {
		wg.Go(func() {
			for range 30 {
				w.DoCycle()
				w.DoCycle()
			}
		})
	}

	wg.Wait()
}

// TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many
// goroutines when no files have changed, testing the early-return
// path where WatchState is read and HasChanges is called.
func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) {
	t.Parallel()
	w, _ := createTestWatcher(t)

	var wg sync.WaitGroup

	for range 16 {
		wg.Go(func() {
			for range 50 {
				w.DoCycle()
			}
		})
	}

	wg.Wait()
}

// TestWatcherAlternatingModifyAndDoCycle alternates between modifying
// a file and calling DoCycle from different goroutines, creating a
// realistic scenario where the file watcher detects changes mid-cycle.
func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) {
	t.Parallel()
	w, sys := createTestWatcher(t)

	var wg sync.WaitGroup

	// Writer goroutine: continuously modifies files
	wg.Go(func() {
		for j := range 100 {
			_ = sys.fsFromFileMap().WriteFile(
				"/home/src/workspaces/project/a.ts",
				fmt.Sprintf("const a: number = %d;", j),
			)
		}
	})

	// Multiple DoCycle goroutines
	for range 4 {
		wg.Go(func() {
			for range 25 {
				w.DoCycle()
			}
		})
	}

	// State reader goroutines
	for range 4 {
		wg.Go(func() {
			for range 100 {
				w.DoCycle()
			}
		})
	}

	wg.Wait()
}

func TestBuildWatchStopsWhenContextIsCancelled(t *testing.T) {
	t.Parallel()

	sys := newTestSys(&tscInput{
		files: FileMap{
			"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"composite":true},"files":["index.ts"]}`,
			"/home/src/workspaces/project/index.ts":      `export const x = 1;`,
		},
	}, false)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	resultCh := make(chan tsc.CommandLineResult, 1)
	go func() {
		resultCh <- execute.CommandLine(ctx, sys, []string{"--build", "--watch", "--watchInterval", "60000"}, sys)
	}()

	select {
	case result := <-resultCh:
		assert.Equal(t, result.Status, tsc.ExitStatusSuccess)
		assert.Assert(t, result.Watcher != nil)
	case <-time.After(2 * time.Second):
		t.Fatal("build watch did not stop after context cancellation")
	}
}
