package ui

import (
	"testing"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

// assertCheckboxWiring verifies a build...Options checkbox is present, correctly
// labeled, and that its OnChanged closure drives the mapped State field in both
// directions. OnChanged is invoked directly (not SetChecked, which no-ops when
// the value is unchanged) so the production callback runs unconditionally.
func assertCheckboxWiring(t *testing.T, c *widget.Check, wantLabel string, getField func() bool) {
	t.Helper()
	if c == nil {
		t.Fatalf("checkbox %q is nil", wantLabel)
	}
	if c.Text != wantLabel {
		t.Fatalf("checkbox label = %q, want %q", c.Text, wantLabel)
	}
	if c.OnChanged == nil {
		t.Fatalf("checkbox %q has nil OnChanged", wantLabel)
	}
	c.OnChanged(true)
	if !getField() {
		t.Fatalf("checkbox %q OnChanged(true) did not flip its State field to true", wantLabel)
	}
	c.OnChanged(false)
	if getField() {
		t.Fatalf("checkbox %q OnChanged(false) did not flip its State field to false", wantLabel)
	}
}

func TestBuildEncryptOptionsWireCheckboxesToState(t *testing.T) {
	newTestFyneApp(t)

	a := createTestApp(t)
	a.advancedContainer = container.NewVBox()

	fyne.DoAndWait(func() {
		a.buildEncryptOptions()

		assertCheckboxWiring(t, a.paranoidCheck, "Paranoid mode", func() bool { return a.State.Paranoid })
		assertCheckboxWiring(t, a.compressCheck, "Compress files", func() bool { return a.State.Compress })
		assertCheckboxWiring(t, a.reedSolomonCheck, "Reed-Solomon", func() bool { return a.State.ReedSolomon })
		assertCheckboxWiring(t, a.deleteCheck, "Delete files", func() bool { return a.State.Delete })
		assertCheckboxWiring(t, a.deniabilityCheck, "Deniability", func() bool { return a.State.Deniability })
		assertCheckboxWiring(t, a.recursivelyCheck, "Recursively", func() bool { return a.State.Recursively })
		assertCheckboxWiring(t, a.splitCheck, "Split:", func() bool { return a.State.Split })
	})
}

func TestBuildDecryptOptionsWireCheckboxesToState(t *testing.T) {
	newTestFyneApp(t)

	a := createTestApp(t)
	a.advancedContainer = container.NewVBox()
	a.State.InputFile = "volume.zip.pcv"
	a.State.AutoUnzip = true

	fyne.DoAndWait(func() {
		a.buildDecryptOptions()

		assertCheckboxWiring(t, a.forceDecryptCheck, "Force decrypt", func() bool { return a.State.Keep })
		assertCheckboxWiring(t, a.verifyFirstCheck, "Verify first", func() bool { return a.State.VerifyFirst })
		assertCheckboxWiring(t, a.deleteVolumeCheck, "Delete volume", func() bool { return a.State.Delete })
		assertCheckboxWiring(t, a.sameLevelCheck, "Same level", func() bool { return a.State.SameLevel })

		// autoUnzip's OnChanged(false) also clears SameLevel, so assert it directly.
		if a.autoUnzipCheck == nil || a.autoUnzipCheck.Text != "Auto unzip" {
			t.Fatalf("autoUnzipCheck missing or mislabeled: %+v", a.autoUnzipCheck)
		}
		a.autoUnzipCheck.OnChanged(true)
		if !a.State.AutoUnzip {
			t.Fatal("Auto unzip OnChanged(true) did not set State.AutoUnzip")
		}
		a.autoUnzipCheck.OnChanged(false)
		if a.State.AutoUnzip {
			t.Fatal("Auto unzip OnChanged(false) did not clear State.AutoUnzip")
		}
	})
}

func TestBuildDecryptOptionsDisableGuards(t *testing.T) {
	newTestFyneApp(t)

	testCases := []struct {
		name             string
		inputFile        string
		autoUnzip        bool
		deniability      bool
		wantAutoUnzipOff bool
		wantSameLevelOff bool
		wantForceOff     bool
	}{
		{"ZipEnablesUnzip", "volume.zip.pcv", true, false, false, false, false},
		{"NonZipDisablesUnzip", "volume.pcv", true, false, true, true, false},
		{"ZipNoAutoUnzipDisablesSameLevel", "volume.zip.pcv", false, false, false, true, false},
		{"DeniabilityDisablesForceDecrypt", "volume.zip.pcv", true, true, false, false, true},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			a := createTestApp(t)
			a.advancedContainer = container.NewVBox()
			a.State.InputFile = tc.inputFile
			a.State.AutoUnzip = tc.autoUnzip
			a.State.Deniability = tc.deniability

			fyne.DoAndWait(func() {
				a.buildDecryptOptions()
			})

			if got := a.autoUnzipCheck.Disabled(); got != tc.wantAutoUnzipOff {
				t.Errorf("autoUnzipCheck.Disabled() = %v, want %v", got, tc.wantAutoUnzipOff)
			}
			if got := a.sameLevelCheck.Disabled(); got != tc.wantSameLevelOff {
				t.Errorf("sameLevelCheck.Disabled() = %v, want %v", got, tc.wantSameLevelOff)
			}
			if got := a.forceDecryptCheck.Disabled(); got != tc.wantForceOff {
				t.Errorf("forceDecryptCheck.Disabled() = %v, want %v", got, tc.wantForceOff)
			}
		})
	}
}

func TestBuildMobileEncryptOptionsWireCheckboxesToState(t *testing.T) {
	newTestFyneApp(t)

	a := createTestApp(t)
	a.advancedContainer = container.NewVBox()

	fyne.DoAndWait(func() {
		a.buildMobileEncryptOptions()

		assertCheckboxWiring(t, a.paranoidCheck, "Paranoid mode", func() bool { return a.State.Paranoid })
		assertCheckboxWiring(t, a.compressCheck, "Compress files", func() bool { return a.State.Compress })
		assertCheckboxWiring(t, a.reedSolomonCheck, "Reed-Solomon", func() bool { return a.State.ReedSolomon })
		assertCheckboxWiring(t, a.deleteCheck, "Delete files", func() bool { return a.State.Delete })
		assertCheckboxWiring(t, a.deniabilityCheck, "Deniability", func() bool { return a.State.Deniability })
		assertCheckboxWiring(t, a.recursivelyCheck, "Recursively", func() bool { return a.State.Recursively })
		assertCheckboxWiring(t, a.splitCheck, "Split:", func() bool { return a.State.Split })
	})
}

func TestBuildMobileDecryptOptionsWireCheckboxesToState(t *testing.T) {
	newTestFyneApp(t)

	a := createTestApp(t)
	a.advancedContainer = container.NewVBox()

	fyne.DoAndWait(func() {
		a.buildMobileDecryptOptions()

		assertCheckboxWiring(t, a.forceDecryptCheck, "Force decrypt", func() bool { return a.State.Keep })
		assertCheckboxWiring(t, a.verifyFirstCheck, "Verify first", func() bool { return a.State.VerifyFirst })
		assertCheckboxWiring(t, a.deleteCheck, "Delete encrypted", func() bool { return a.State.Delete })
	})
}

// TestEncryptAdvancedOptionsNeverSoftLock guards against issue #56, where certain
// advanced-option combinations left the encrypt checkboxes permanently disabled,
// forcing the user to restart the app to recover. The fix makes every disabled
// state a pure function of the current UISnapshot (updateEncryptOptionsState) with
// no latched flags and no mutual exclusion, so any selection is always reversible.
// These cases assert that the two combinations reported in #56 keep every option
// interactive: the user can always toggle back out without restarting.
func TestEncryptAdvancedOptionsNeverSoftLock(t *testing.T) {
	newTestFyneApp(t)

	// newEncryptAppWithCredentials builds an encrypt-mode app whose credentials make
	// advancedDisabled==false (password set and matching) and whose >1 file count
	// keeps "Recursively" ungated, so any disabled checkbox can only come from
	// option interplay — which is exactly what #56 was about.
	newEncryptAppWithCredentials := func(t *testing.T) *App {
		t.Helper()
		a := createTestApp(t)
		a.advancedContainer = container.NewVBox()
		a.State.Mode = "encrypt"
		a.State.Password = "correct horse"
		a.State.CPassword = "correct horse"
		a.State.AllFiles = []string{"a.txt", "b.txt"}
		fyne.DoAndWait(func() { a.buildEncryptOptions() })
		return a
	}

	// recompute runs the production disable-state recomputation from the current
	// State snapshot, exactly as the app does after every UI change.
	recompute := func(a *App) {
		fyne.DoAndWait(func() { a.updateAdvancedDisableState() })
	}

	// Reported combo (b): Compress + Recursively. Enabling Recursively disables
	// Compress (intended), but disabling Recursively again must restore it.
	t.Run("CompressRecoversAfterUncheckingRecursively", func(t *testing.T) {
		a := newEncryptAppWithCredentials(t)

		recompute(a)
		if a.compressCheck.Disabled() {
			t.Fatal("precondition: Compress should be enabled before Recursively is checked")
		}

		// User checks Recursively (its OnChanged also clears Compress).
		a.State.Recursively = true
		a.State.Compress = false
		recompute(a)
		if !a.compressCheck.Disabled() {
			t.Fatal("Compress should be disabled while Recursively is checked")
		}
		if a.recursivelyCheck.Disabled() {
			t.Fatal("Recursively must stay enabled so the user can undo it")
		}

		// User unchecks Recursively: Compress MUST become usable again. A latched
		// disable (the #56 soft-lock) would leave it stuck disabled here.
		a.State.Recursively = false
		recompute(a)
		if a.compressCheck.Disabled() {
			t.Fatal("#56 regression: Compress stayed disabled after Recursively was unchecked (soft-lock)")
		}
	})

	// Reported combo (a): Paranoid + Reed-Solomon + Deniability. None of these
	// mutually exclude, so selecting all three must not disable any option.
	t.Run("ParanoidReedSolomonDeniabilityLockNothing", func(t *testing.T) {
		a := newEncryptAppWithCredentials(t)

		a.State.Paranoid = true
		a.State.ReedSolomon = true
		a.State.Deniability = true
		recompute(a)

		for _, c := range []struct {
			name string
			box  *widget.Check
		}{
			{"Paranoid mode", a.paranoidCheck},
			{"Compress files", a.compressCheck},
			{"Reed-Solomon", a.reedSolomonCheck},
			{"Delete files", a.deleteCheck},
			{"Deniability", a.deniabilityCheck},
			{"Recursively", a.recursivelyCheck},
			{"Split:", a.splitCheck},
		} {
			if c.box.Disabled() {
				t.Errorf("#56 regression: %q is disabled under Paranoid+Reed-Solomon+Deniability; this combo must not lock any option", c.name)
			}
		}
	})
}
