package fs

import (
	"errors"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	gerr "galaxy/error"
	"galaxy/model/client"
	"galaxy/model/order"
	"galaxy/model/report"

	"github.com/google/uuid"
)

const testTimeout = time.Second

type callbackResult[T any] struct {
	value T
	err   error
}

func TestStateRoundTrip(t *testing.T) {
	s := newTestStorage(t)
	want := sampleState()

	exists, err := s.StateExists()
	if err != nil {
		t.Fatalf("state exists before save: %v", err)
	}
	if exists {
		t.Fatal("state file should not exist before save")
	}

	if err := s.SaveState(want); err != nil {
		t.Fatalf("save state: %v", err)
	}

	exists, err = s.StateExists()
	if err != nil {
		t.Fatalf("state exists after save: %v", err)
	}
	if !exists {
		t.Fatal("state file should exist after save")
	}

	got, err := s.LoadState()
	if err != nil {
		t.Fatalf("load state: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("loaded state mismatch\nwant: %#v\ngot:  %#v", want, got)
	}
}

func TestStateRoundTripAsync(t *testing.T) {
	s := newTestStorage(t)
	want := sampleState()

	saveDone := make(chan error, 1)
	s.SaveStateAsync(want, func(err error) {
		saveDone <- err
	})
	if err := waitError(t, saveDone); err != nil {
		t.Fatalf("save state: %v", err)
	}

	existsDone := make(chan callbackResult[bool], 1)
	s.StateExistsAsync(func(ok bool, err error) {
		existsDone <- callbackResult[bool]{value: ok, err: err}
	})
	exists := waitResult(t, existsDone)
	if exists.err != nil {
		t.Fatalf("state exists: %v", exists.err)
	}
	if !exists.value {
		t.Fatal("state file should exist after save")
	}

	loadDone := make(chan callbackResult[client.State], 1)
	s.LoadStateAsync(func(state client.State, err error) {
		loadDone <- callbackResult[client.State]{value: state, err: err}
	})
	got := waitResult(t, loadDone)
	if got.err != nil {
		t.Fatalf("load state: %v", got.err)
	}
	if !reflect.DeepEqual(got.value, want) {
		t.Fatalf("loaded state mismatch\nwant: %#v\ngot:  %#v", want, got.value)
	}
}

func TestReportAndOrderRoundTripAsync(t *testing.T) {
	s := newTestStorage(t)
	id := client.GameID("game-1")
	turn := uint(7)
	initialReport := sampleReport(turn, "Terran")
	updatedReport := sampleReport(turn, "Zenith")
	wantOrder := sampleOrder()

	saveReportDone := make(chan error, 1)
	s.SaveReportAsync(id, turn, initialReport, func(err error) {
		saveReportDone <- err
	})
	if err := waitError(t, saveReportDone); err != nil {
		t.Fatalf("save report: %v", err)
	}

	saveOrderDone := make(chan error, 1)
	s.SaveOrderAsync(id, turn, wantOrder, func(err error) {
		saveOrderDone <- err
	})
	if err := waitError(t, saveOrderDone); err != nil {
		t.Fatalf("save order: %v", err)
	}

	saveUpdatedReportDone := make(chan error, 1)
	s.SaveReportAsync(id, turn, updatedReport, func(err error) {
		saveUpdatedReportDone <- err
	})
	if err := waitError(t, saveUpdatedReportDone); err != nil {
		t.Fatalf("save updated report: %v", err)
	}

	loadReportDone := make(chan callbackResult[report.Report], 1)
	s.LoadReportAsync(id, turn, func(rep report.Report, err error) {
		loadReportDone <- callbackResult[report.Report]{value: rep, err: err}
	})
	gotReport := waitResult(t, loadReportDone)
	if gotReport.err != nil {
		t.Fatalf("load report: %v", gotReport.err)
	}
	if !reflect.DeepEqual(gotReport.value, updatedReport) {
		t.Fatalf("loaded report mismatch\nwant: %#v\ngot:  %#v", updatedReport, gotReport.value)
	}

	loadOrderDone := make(chan callbackResult[order.UserGamesOrder], 1)
	s.LoadOrderAsync(id, turn, func(got order.UserGamesOrder, err error) {
		loadOrderDone <- callbackResult[order.UserGamesOrder]{value: got, err: err}
	})
	gotOrder := waitResult(t, loadOrderDone)
	if gotOrder.err != nil {
		t.Fatalf("load order: %v", gotOrder.err)
	}
	if !reflect.DeepEqual(gotOrder.value, wantOrder) {
		t.Fatalf("loaded order mismatch\nwant: %#v\ngot:  %#v", wantOrder, gotOrder.value)
	}
}

func TestSaveOrderBeforeReportReturnsNotExist(t *testing.T) {
	s := newTestStorage(t)

	done := make(chan error, 1)
	s.SaveOrderAsync("game-2", 3, sampleOrder(), func(err error) {
		done <- err
	})
	err := waitError(t, done)
	if !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("save order error = %v, want os.ErrNotExist", err)
	}
}

func TestRawFileCRUDAndList(t *testing.T) {
	s := newTestStorage(t)

	if err := s.WriteFile("/nested/alpha.txt", []byte("alpha")); err != nil {
		t.Fatalf("write alpha: %v", err)
	}
	if err := s.WriteFile("beta.txt", []byte("beta")); err != nil {
		t.Fatalf("write beta: %v", err)
	}

	alphaExists, alphaPath, err := s.FileExists("nested/alpha.txt")
	if err != nil {
		t.Fatalf("file exists: %v", err)
	}
	if !alphaExists {
		t.Fatal("nested/alpha.txt should exist")
	}
	wantAlphaPath := filepath.Join(s.storageRoot, "nested", "alpha.txt")
	if alphaPath != wantAlphaPath {
		t.Fatalf("file path = %q, want %q", alphaPath, wantAlphaPath)
	}

	missingExists, missingPath, err := s.FileExists("missing.txt")
	if err != nil {
		t.Fatalf("missing file exists: %v", err)
	}
	if missingExists {
		t.Fatal("missing.txt should not exist")
	}
	wantMissingPath := filepath.Join(s.storageRoot, "missing.txt")
	if wantMissingPath != missingPath {
		t.Fatalf("missing file path = %q, want %q", missingPath, wantMissingPath)
	}

	alphaData, err := s.ReadFile("nested/alpha.txt")
	if err != nil {
		t.Fatalf("read alpha: %v", err)
	}
	if string(alphaData) != "alpha" {
		t.Fatalf("read alpha = %q, want %q", alphaData, "alpha")
	}

	if err := os.WriteFile(filepath.Join(s.storageRoot, "skip.txt"+newFileSuffix), []byte("tmp"), 0o644); err != nil {
		t.Fatalf("create stale .new file: %v", err)
	}
	if err := os.WriteFile(filepath.Join(s.storageRoot, "skip.txt"+oldFileSuffix), []byte("tmp"), 0o644); err != nil {
		t.Fatalf("create stale .old file: %v", err)
	}

	files, err := s.ListFiles()
	if err != nil {
		t.Fatalf("list files: %v", err)
	}
	wantFiles := []string{
		"beta.txt",
		filepath.Join("nested", "alpha.txt"),
	}
	if !reflect.DeepEqual(files, wantFiles) {
		t.Fatalf("listed files mismatch\nwant: %#v\ngot:  %#v", wantFiles, files)
	}

	if err := s.DeleteFile("beta.txt"); err != nil {
		t.Fatalf("delete beta: %v", err)
	}
	if err := s.DeleteFile("beta.txt"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("delete missing beta error = %v, want os.ErrNotExist", err)
	}
}

func TestPathTraversalRejected(t *testing.T) {
	s := newTestStorage(t)

	for _, path := range []string{"../escape.txt", "..\\escape.txt", ""} {
		t.Run(path, func(t *testing.T) {
			err := s.WriteFile(path, []byte("blocked"))
			if err == nil {
				t.Fatalf("write %q unexpectedly succeeded", path)
			}
			if !gerr.IsStorage(err) {
				t.Fatalf("write %q error = %v, want storage classified error", path, err)
			}
		})
	}
}

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

	s := newTestStorage(t)

	err := s.DeleteFile("missing.txt")
	if !gerr.IsStorage(err) {
		t.Fatalf("DeleteFile() error = %v, want storage classified error", err)
	}
	if !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("DeleteFile() error = %v, want os.ErrNotExist", err)
	}
}

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

	s := newTestStorage(t)
	if err := os.WriteFile(filepath.Join(s.storageRoot, stateFileName), []byte("{"), 0o644); err != nil {
		t.Fatalf("seed invalid state file: %v", err)
	}

	_, err := s.LoadState()
	if err == nil {
		t.Fatal("LoadState() error = nil, want non-nil")
	}
	if !gerr.IsStorage(err) {
		t.Fatalf("LoadState() error = %v, want storage classified error", err)
	}
}

func TestAtomicWriteFirstAndOverwrite(t *testing.T) {
	s := newTestStorage(t)
	target := filepath.Join("turns", "12.bin")

	if err := s.WriteFile(target, []byte("first")); err != nil {
		t.Fatalf("first write: %v", err)
	}
	assertFileContent(t, s, target, "first")
	assertNoTempArtifacts(t, s, target)

	if err := s.WriteFile(target, []byte("second")); err != nil {
		t.Fatalf("overwrite: %v", err)
	}
	assertFileContent(t, s, target, "second")
	assertNoTempArtifacts(t, s, target)
}

func TestAtomicWriteStaleTempCollision(t *testing.T) {
	t.Run("stale new file", func(t *testing.T) {
		s := newTestStorage(t)
		target := "collision-new.txt"
		absTarget, err := s.resolvePath(target)
		if err != nil {
			t.Fatalf("resolve target: %v", err)
		}
		if err := os.MkdirAll(filepath.Dir(absTarget), os.ModePerm); err != nil {
			t.Fatalf("create parent dir: %v", err)
		}
		if err := os.WriteFile(absTarget+newFileSuffix, []byte("stale"), 0o644); err != nil {
			t.Fatalf("write stale new file: %v", err)
		}

		err = s.WriteFile(target, []byte("payload"))
		if err == nil || !strings.Contains(err.Error(), "new file already exists") {
			t.Fatalf("write error = %v, want stale new file error", err)
		}
	})

	t.Run("stale old file", func(t *testing.T) {
		s := newTestStorage(t)
		target := "collision-old.txt"
		absTarget, err := s.resolvePath(target)
		if err != nil {
			t.Fatalf("resolve target: %v", err)
		}
		if err := os.WriteFile(absTarget, []byte("current"), 0o644); err != nil {
			t.Fatalf("write target: %v", err)
		}
		if err := os.WriteFile(absTarget+oldFileSuffix, []byte("stale"), 0o644); err != nil {
			t.Fatalf("write stale old file: %v", err)
		}

		err = s.WriteFile(target, []byte("payload"))
		if err == nil || !strings.Contains(err.Error(), "old file already exists") {
			t.Fatalf("write error = %v, want stale old file error", err)
		}
	})
}

func TestAtomicWriteRollbackOnRenameFailure(t *testing.T) {
	s := newTestStorage(t)
	target := filepath.Join("rollback", "state.txt")
	absTarget, err := s.resolvePath(target)
	if err != nil {
		t.Fatalf("resolve target: %v", err)
	}

	if err := s.WriteFile(target, []byte("original")); err != nil {
		t.Fatalf("seed target file: %v", err)
	}

	origRename := s.renameFileFn
	s.renameFileFn = func(oldPath, newPath string) error {
		if oldPath == absTarget+newFileSuffix && newPath == absTarget {
			return errors.New("forced rename failure")
		}
		return origRename(oldPath, newPath)
	}

	err = s.WriteFile(target, []byte("replacement"))
	if err == nil || !strings.Contains(err.Error(), "forced rename failure") {
		t.Fatalf("write error = %v, want forced rename failure", err)
	}

	assertFileContent(t, s, target, "original")
	assertNoTempArtifacts(t, s, target)
}

func TestSamePathOperationsSerialize(t *testing.T) {
	s := newTestStorage(t)
	target := "shared.txt"
	absTarget, err := s.resolvePath(target)
	if err != nil {
		t.Fatalf("resolve target: %v", err)
	}

	entered := make(chan struct{})
	release := make(chan struct{})
	origWrite := s.writeFileFn
	var writes atomic.Int32
	s.writeFileFn = func(path string, data []byte, perm os.FileMode) error {
		if path == absTarget+newFileSuffix && writes.Add(1) == 1 {
			close(entered)
			<-release
		}
		return origWrite(path, data, perm)
	}

	firstDone := make(chan error, 1)
	go func() {
		firstDone <- s.WriteFile(target, []byte("one"))
	}()
	waitSignal(t, entered, "first write entered")

	secondDone := make(chan error, 1)
	go func() {
		secondDone <- s.WriteFile(target, []byte("two"))
	}()

	select {
	case err := <-secondDone:
		t.Fatalf("second write finished before first released: %v", err)
	case <-time.After(50 * time.Millisecond):
	}
	if writes.Load() != 1 {
		t.Fatalf("same-path write reached file hook %d times before release, want 1", writes.Load())
	}

	close(release)
	if err := waitError(t, firstDone); err != nil {
		t.Fatalf("first write: %v", err)
	}
	if err := waitError(t, secondDone); err != nil {
		t.Fatalf("second write: %v", err)
	}
}

func TestDifferentPathOperationsDoNotBlockEachOther(t *testing.T) {
	s := newTestStorage(t)
	blockedTarget := "blocked.txt"
	absTarget, err := s.resolvePath(blockedTarget)
	if err != nil {
		t.Fatalf("resolve blocked target: %v", err)
	}

	entered := make(chan struct{})
	release := make(chan struct{})
	origWrite := s.writeFileFn
	s.writeFileFn = func(path string, data []byte, perm os.FileMode) error {
		if path == absTarget+newFileSuffix {
			close(entered)
			<-release
		}
		return origWrite(path, data, perm)
	}

	blockedDone := make(chan error, 1)
	go func() {
		blockedDone <- s.WriteFile(blockedTarget, []byte("blocked"))
	}()
	waitSignal(t, entered, "blocked write entered")

	freeDone := make(chan error, 1)
	go func() {
		freeDone <- s.WriteFile("free.txt", []byte("free"))
	}()

	select {
	case err := <-freeDone:
		if err != nil {
			t.Fatalf("free write: %v", err)
		}
	case <-time.After(testTimeout):
		t.Fatal("write for a different path should not block")
	}

	close(release)
	if err := waitError(t, blockedDone); err != nil {
		t.Fatalf("blocked write: %v", err)
	}
}

func TestSaveStateIsNonBlockingAndCallbackBased(t *testing.T) {
	s := newTestStorage(t)

	entered := make(chan struct{})
	release := make(chan struct{})
	origWrite := s.writeFileFn
	s.writeFileFn = func(path string, data []byte, perm os.FileMode) error {
		close(entered)
		<-release
		return origWrite(path, data, perm)
	}

	callbacks := make(chan error, 2)
	s.SaveStateAsync(sampleState(), func(err error) {
		callbacks <- err
	})

	waitSignal(t, entered, "async save entered")

	select {
	case err := <-callbacks:
		t.Fatalf("callback fired before storage write completed: %v", err)
	default:
	}

	close(release)
	if err := waitError(t, callbacks); err != nil {
		t.Fatalf("callback error: %v", err)
	}

	select {
	case err := <-callbacks:
		t.Fatalf("callback fired more than once: %v", err)
	case <-time.After(50 * time.Millisecond):
	}
}

func newTestStorage(t *testing.T) *fsStorage {
	t.Helper()

	s, err := NewFS(t.TempDir())
	if err != nil {
		t.Fatalf("new test storage: %v", err)
	}
	return s
}

func sampleState() client.State {
	return client.State{
		GameState: []client.GameState{
			{ID: client.GameID("game-1"), LastTurn: 12, ActiveTurn: 11},
			{ID: client.GameID("game-2"), LastTurn: 4, ActiveTurn: 4},
		},
		ActiveGameID: new(client.GameID("game-2")),
	}
}

func sampleReport(turn uint, race string) report.Report {
	return report.Report{
		Turn:        turn,
		Width:       160,
		Height:      90,
		PlanetCount: 8,
		Race:        race,
		VoteFor:     "assembly",
	}
}

func sampleOrder() order.UserGamesOrder {
	return order.UserGamesOrder{
		GameID:    uuid.New(),
		UpdatedAt: 1700,
		Commands: []order.DecodableCommand{
			&order.CommandPlanetRename{
				CommandMeta: order.CommandMeta{
					CmdType: order.CommandTypePlanetRename,
					CmdID:   "rename-planet",
				},
				Number: 2,
				Name:   "Nova Prime",
			},
			&order.CommandRaceVote{
				CommandMeta: order.CommandMeta{
					CmdType: order.CommandTypeRaceVote,
					CmdID:   "vote-race",
				},
				Acceptor: "ZENITH",
			},
		},
	}
}

func assertFileContent(t *testing.T, s *fsStorage, path, want string) {
	t.Helper()

	got, err := s.ReadFile(path)
	if err != nil {
		t.Fatalf("read %q: %v", path, err)
	}
	if string(got) != want {
		t.Fatalf("content for %q = %q, want %q", path, got, want)
	}
}

func assertNoTempArtifacts(t *testing.T, s *fsStorage, path string) {
	t.Helper()

	absPath, err := s.resolvePath(path)
	if err != nil {
		t.Fatalf("resolve path %q: %v", path, err)
	}
	for _, tempPath := range []string{absPath + newFileSuffix, absPath + oldFileSuffix} {
		if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) {
			t.Fatalf("temp artifact %q should not exist, stat err = %v", tempPath, err)
		}
	}
}

func waitSignal(t *testing.T, ch <-chan struct{}, name string) {
	t.Helper()

	select {
	case <-ch:
	case <-time.After(testTimeout):
		t.Fatalf("timeout waiting for %s", name)
	}
}

func waitError(t *testing.T, ch <-chan error) error {
	t.Helper()

	select {
	case err := <-ch:
		return err
	case <-time.After(testTimeout):
		t.Fatal("timeout waiting for error callback")
		return nil
	}
}

func waitResult[T any](t *testing.T, ch <-chan callbackResult[T]) callbackResult[T] {
	t.Helper()

	select {
	case result := <-ch:
		return result
	case <-time.After(testTimeout):
		t.Fatal("timeout waiting for callback result")
		return callbackResult[T]{}
	}
}
