package appexport

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// v0.124.0 Part 3 (§7D) — the .fab loop's proof at unit level: a real export through
// executeExport produces a bundle that a real executeImport restores to identical content, and
// a corrupted downloaded copy is REFUSED (the .fab's integrity is the gzip CRC + the manifest
// segment validation — there is no per-file checksum; corruption breaks the extract, and the
// import must fail loudly, not restore garbage).

// rtProvider is a filesystem-only fake: a config-only app (no HDD, no volumes, no DB) so the
// whole loop runs without docker.
type rtProvider struct {
	stackDir  string
	stacksDir string
	deployed  bool
	running   bool
	volumes   []string
	started   bool
	stopped   int
	removed   int
	savedEnv  map[string]string
}

func (p *rtProvider) GetStackDir(string) (string, bool) { return p.stackDir, true }
func (p *rtProvider) GetStackComposePath(string) (string, bool) {
	return filepath.Join(p.stackDir, "docker-compose.yml"), true
}
func (p *rtProvider) GetStackHDDMounts(string) []string          { return nil }
func (p *rtProvider) GetStackHDDPath(string) string              { return "" }
func (p *rtProvider) IsStackRunning(string) bool                 { return p.running }
func (p *rtProvider) StopStack(string) error                     { p.stopped++; return nil }
func (p *rtProvider) StartStack(string) error                    { p.started = true; return nil }
func (p *rtProvider) GetStackDisplayName(n string) string        { return "RT " + n }
func (p *rtProvider) GetStackNeedsHDD(string) bool               { return false }
func (p *rtProvider) GetDockerVolumes(string) []string           { return p.volumes }
func (p *rtProvider) IsStackDeployed(string) bool                { return p.deployed }
func (p *rtProvider) GetDecryptedEnv(string) map[string]string   { return nil }
func (p *rtProvider) GetStacksBaseDir() string                   { return p.stacksDir }
func (p *rtProvider) RefreshStacks() error                       { return nil }
func (p *rtProvider) RemoveStackVolumes(string) error            { p.removed++; return nil }
func (p *rtProvider) SaveEncryptedAppConfig(stackDir string, env map[string]string) error {
	p.savedEnv = env
	return nil
}

func waitJob(t *testing.T, e *Exporter) *Job {
	t.Helper()
	deadline := time.Now().Add(30 * time.Second)
	for time.Now().Before(deadline) {
		job := e.GetActiveJob()
		if job != nil {
			job.mu.RLock()
			done := job.Done
			job.mu.RUnlock()
			if done {
				return job
			}
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Fatal("job did not finish in time")
	return nil
}

func jobErr(j *Job) string {
	j.mu.RLock()
	defer j.mu.RUnlock()
	if j.Error != "" {
		return j.Error
	}
	for _, s := range j.Steps {
		if s.Status == "failed" {
			return s.Error
		}
	}
	return ""
}

func TestFabRoundTrip_ExportImportContentEquality(t *testing.T) {
	const stack = "rt-app"
	lg := log.New(io.Discard, "", 0)

	// Source stack: a compose file + a marker config with known content.
	srcStack := t.TempDir()
	compose := "services:\n  rt-app:\n    image: alpine\n"
	marker := "MARKER-CONTENT-42\n"
	os.WriteFile(filepath.Join(srcStack, "docker-compose.yml"), []byte(compose), 0644)
	os.WriteFile(filepath.Join(srcStack, "settings.conf"), []byte(marker), 0644)

	drive := t.TempDir()
	prov := &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}
	e := NewExporter(prov, lg, "test")

	// --- export (the REAL pipeline; same producer as a drive export) ---
	if err := e.StartExport(ExportRequest{StackName: stack, DestDrive: drive}); err != nil {
		t.Fatalf("StartExport: %v", err)
	}
	job := waitJob(t, e)
	if msg := jobErr(job); msg != "" {
		t.Fatalf("export failed: %s", msg)
	}
	entries, _ := os.ReadDir(ExportDir(drive))
	var fabPath string
	for _, en := range entries {
		if strings.HasSuffix(en.Name(), ".fab") {
			fabPath = filepath.Join(ExportDir(drive), en.Name())
		}
	}
	if fabPath == "" {
		t.Fatal("no .fab produced")
	}

	// The manifest is readable and names the app (what /api/export/manifest shows pre-import).
	man, err := ReadManifestFromFAB(fabPath)
	if err != nil {
		t.Fatalf("manifest: %v", err)
	}
	if man.AppName != stack {
		t.Fatalf("manifest app = %q", man.AppName)
	}

	// --- corrupted copy must be REFUSED (assert the refusal, §10 red-proof of the loop) ---
	corrupt := filepath.Join(t.TempDir(), "corrupt.fab")
	raw, _ := os.ReadFile(fabPath)
	mid := len(raw) / 2
	raw[mid] ^= 0xFF
	raw[mid+1] ^= 0xFF
	os.WriteFile(corrupt, raw, 0644)

	prov2 := &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: false}
	e2 := NewExporter(prov2, lg, "test")
	if err := e2.StartImport(ImportRequest{FABPath: corrupt}); err != nil {
		t.Fatalf("StartImport(corrupt) should start (refusal is async): %v", err)
	}
	job = waitJob(t, e2)
	if msg := jobErr(job); msg == "" {
		t.Fatal("a corrupted bundle must FAIL the import (gzip CRC), got success")
	}
	if prov2.started {
		t.Fatal("a refused import must not start the app")
	}
	if _, err := os.Stat(filepath.Join(prov2.stacksDir, stack, "settings.conf")); !os.IsNotExist(err) {
		t.Fatal("a refused import must not restore content")
	}

	// --- the clean bundle round-trips: restored content is byte-identical ---
	prov3 := &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: false}
	e3 := NewExporter(prov3, lg, "test")
	if err := e3.StartImport(ImportRequest{FABPath: fabPath}); err != nil {
		t.Fatalf("StartImport: %v", err)
	}
	job = waitJob(t, e3)
	if msg := jobErr(job); msg != "" {
		t.Fatalf("import failed: %s", msg)
	}
	restoredStack := filepath.Join(prov3.stacksDir, stack)
	for name, want := range map[string]string{
		"docker-compose.yml": compose,
		"settings.conf":      marker,
	} {
		got, err := os.ReadFile(filepath.Join(restoredStack, name))
		if err != nil {
			t.Fatalf("restored %s missing: %v", name, err)
		}
		if string(got) != want {
			t.Errorf("restored %s differs:\n got %q\nwant %q", name, got, want)
		}
	}
	if !prov3.started {
		t.Error("import must start the restored app")
	}
}
