package updates

import (
	"context"
	"errors"
	"io"
	"path/filepath"
	"sync"
	"testing"
	"time"
)

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

	version, err := NormalizeVersion("1.2.3")
	if err != nil {
		t.Fatalf("NormalizeVersion returned error: %v", err)
	}
	if version != "v1.2.3" {
		t.Fatalf("NormalizeVersion returned %q", version)
	}
}

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

	platform := &fakePlatform{
		root: InstallRoot{Path: t.TempDir()},
		stage: StagedArtifact{
			Path: t.TempDir(),
			Root: InstallRoot{Path: t.TempDir()},
			Bundle: BundleManifest{
				SchemaVersion: 1,
				EntryPoint:    "App",
				Files: []BundleFile{{Path: "App", Mode: "0755"}},
			},
		},
	}
	release := &Release{
		ID:      "1.1.0",
		Version: "1.1.0",
		Channel: ChannelStable,
		Artifact: Artifact{
			Kind:   ArtifactKindBundleArchive,
			Format: ArtifactFormatZip,
			URL:    "https://example.invalid/app.zip",
			SHA256: "deadbeef",
		},
	}

	controller, err := NewController(Config{
		App: AppDescriptor{
			ProductID:      "com.example.app",
			CurrentVersion: "1.0.0",
			Channel:        ChannelStable,
			OS:             "linux",
			Arch:           "amd64",
			ExecutablePath: filepath.Join(t.TempDir(), "App"),
		},
		Provider:   fakeProvider{release: release},
		Downloader: fakeDownloader{file: DownloadedFile{Path: "bundle.zip", SHA256: "deadbeef"}},
		Store:      &memoryStore{},
		Platform:   platform,
	})
	if err != nil {
		t.Fatalf("NewController returned error: %v", err)
	}

	snapshot, err := controller.Check(context.Background(), CheckRequest{})
	if err != nil {
		t.Fatalf("Check returned error: %v", err)
	}
	if snapshot.State != StateUpdateAvailable {
		t.Fatalf("Check state = %s", snapshot.State)
	}

	snapshot, err = controller.Download(context.Background())
	if err != nil {
		t.Fatalf("Download returned error: %v", err)
	}
	if snapshot.State != StateReadyToApply {
		t.Fatalf("Download state = %s", snapshot.State)
	}
	if snapshot.Staged == nil {
		t.Fatal("Download did not stage artifact")
	}
}

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

	block := make(chan struct{})
	controller, err := NewController(Config{
		App: AppDescriptor{
			ProductID:      "com.example.app",
			CurrentVersion: "1.0.0",
			Channel:        ChannelStable,
			OS:             "linux",
			Arch:           "amd64",
			ExecutablePath: filepath.Join(t.TempDir(), "App"),
		},
		Provider: fakeProvider{
			resolveFunc: func(context.Context, ResolveRequest) (*Release, error) {
				<-block
				return nil, nil
			},
		},
		Downloader: fakeDownloader{},
		Store:      &memoryStore{},
		Platform:   &fakePlatform{root: InstallRoot{Path: t.TempDir()}},
	})
	if err != nil {
		t.Fatalf("NewController returned error: %v", err)
	}

	done := make(chan error, 1)
	go func() {
		_, err := controller.Check(context.Background(), CheckRequest{})
		done <- err
	}()

	time.Sleep(50 * time.Millisecond)
	if _, err := controller.Check(context.Background(), CheckRequest{}); !errors.Is(err, ErrBusy) {
		t.Fatalf("expected ErrBusy, got %v", err)
	}
	close(block)
	if err := <-done; err != nil {
		t.Fatalf("first Check returned error: %v", err)
	}
}

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

	store := &memoryStore{}
	err := store.Save(context.Background(), Snapshot{
		State: StateRestarting,
		Staged: &StagedArtifact{
			Release: Release{Version: "1.2.0"},
		},
		Candidate: &Release{Version: "1.2.0"},
	})
	if err != nil {
		t.Fatalf("Save returned error: %v", err)
	}

	controller, err := NewController(Config{
		App: AppDescriptor{
			ProductID:      "com.example.app",
			CurrentVersion: "1.2.0",
			Channel:        ChannelStable,
			OS:             "linux",
			Arch:           "amd64",
			ExecutablePath: filepath.Join(t.TempDir(), "App"),
		},
		Provider:   fakeProvider{},
		Downloader: fakeDownloader{},
		Store:      store,
		Platform:   &fakePlatform{root: InstallRoot{Path: t.TempDir()}},
	})
	if err != nil {
		t.Fatalf("NewController returned error: %v", err)
	}

	snapshot := controller.Snapshot()
	if snapshot.Staged != nil {
		t.Fatal("expected staged update to be cleared")
	}
	if snapshot.State != StateUpToDate {
		t.Fatalf("snapshot state = %s", snapshot.State)
	}
}

type fakeProvider struct {
	release     *Release
	resolveFunc func(context.Context, ResolveRequest) (*Release, error)
}

func (provider fakeProvider) Resolve(ctx context.Context, req ResolveRequest) (*Release, error) {
	if provider.resolveFunc != nil {
		return provider.resolveFunc(ctx, req)
	}
	return provider.release, nil
}

func (provider fakeProvider) OpenArtifact(context.Context, Release) (io.ReadCloser, error) {
	return nil, nil
}

type fakeDownloader struct {
	file DownloadedFile
	err  error
}

func (downloader fakeDownloader) Download(context.Context, Artifact) (DownloadedFile, error) {
	return downloader.file, downloader.err
}

type fakePlatform struct {
	root  InstallRoot
	stage StagedArtifact
	err   error
}

func (platform *fakePlatform) DetectInstallRoot(AppDescriptor) (InstallRoot, error) {
	return platform.root, platform.err
}

func (platform *fakePlatform) Stage(context.Context, InstallRoot, DownloadedFile, Release) (StagedArtifact, error) {
	return platform.stage, platform.err
}

func (platform *fakePlatform) SpawnApplyAndRestart(context.Context, ApplyRequest) error {
	return platform.err
}

type memoryStore struct {
	mu       sync.Mutex
	snapshot Snapshot
}

func (store *memoryStore) Load(context.Context) (Snapshot, error) {
	store.mu.Lock()
	defer store.mu.Unlock()
	return store.snapshot, nil
}

func (store *memoryStore) Save(_ context.Context, snapshot Snapshot) error {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.snapshot = snapshot
	return nil
}

func (store *memoryStore) ClearStaged(context.Context) error {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.snapshot.Staged = nil
	store.snapshot.Candidate = nil
	return nil
}
