package ui

import (
	"testing"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"

	ttwidget "github.com/dweymouth/fyne-tooltip/widget"
)

// assertCheckboxWiring verifies a build...Options checkbox is present, labeled
// (non-empty), and that its OnChanged closure drives the mapped State field in
// both directions. The bidirectional OnChanged(true/false)→State assertion is the
// load-bearing part: it catches a checkbox wired to the wrong State field or with
// an inverted bool. OnChanged is invoked directly (not SetChecked, which no-ops
// when the value is unchanged) so the production callback runs unconditionally.
// The label is only checked for non-emptiness (an unlabeled control is a bug);
// wantLabel is retained solely to identify the control in failure messages.
func assertCheckboxWiring(t *testing.T, c *ttwidget.Check, wantLabel string, getField func() bool) {
	t.Helper()
	if c == nil {
		t.Fatalf("checkbox %q is nil", wantLabel)
	}
	if c.Text == "" {
		t.Fatalf("checkbox %q has an empty label", 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  *ttwidget.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)
			}
		}
	})
}

// sharedSecurityWarning is the deliberately-shared tooltip on the two
// "dangerous" encrypt options (Deniability and Recursively). It is asserted
// verbatim below because the exact safety wording is intentional copy; the rest
// of the suite only checks that tooltips are present and otherwise distinct.
const sharedSecurityWarning = "Warning: only use this if you know what it does!"

// assertTooltipsPresentAndDistinct asserts every control carries a non-empty
// tooltip (an empty tooltip is a missing-tooltip bug, issue #79) and that the
// tooltips are distinct across the *non-warning* controls — duplicate copy on
// two unrelated controls almost always means a copy/paste wiring mistake. The
// deliberately-shared security warning is excluded from the distinctness set and
// pinned verbatim by the callers instead.
func assertTooltipsPresentAndDistinct(t *testing.T, controls []struct {
	name string
	tt   interface{ ToolTip() string }
},
) {
	t.Helper()
	seen := make(map[string]string)
	for _, c := range controls {
		got := c.tt.ToolTip()
		if got == "" {
			t.Errorf("%s has an empty tooltip", c.name)
			continue
		}
		if got == sharedSecurityWarning {
			continue // distinctness is not required for the shared warning
		}
		if prev, dup := seen[got]; dup {
			t.Errorf("%s and %s share tooltip %q; tooltips must be distinct", prev, c.name, got)
		}
		seen[got] = c.name
	}
}

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

	t.Run("encrypt", func(t *testing.T) {
		a := createTestApp(t)
		a.advancedContainer = container.NewVBox()
		a.buildEncryptOptions()

		assertTooltipsPresentAndDistinct(t, []struct {
			name string
			tt   interface{ ToolTip() string }
		}{
			{"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},
			{"split units", a.splitUnitSelect},
		})

		// The two dangerous options deliberately share the exact safety wording;
		// pin it verbatim so the warning copy can't be silently softened.
		if got := a.deniabilityCheck.ToolTip(); got != sharedSecurityWarning {
			t.Errorf("Deniability tooltip = %q, want %q", got, sharedSecurityWarning)
		}
		if got := a.recursivelyCheck.ToolTip(); got != sharedSecurityWarning {
			t.Errorf("Recursively tooltip = %q, want %q", got, sharedSecurityWarning)
		}
	})

	t.Run("decrypt", func(t *testing.T) {
		a := createTestApp(t)
		a.advancedContainer = container.NewVBox()
		a.buildDecryptOptions()

		assertTooltipsPresentAndDistinct(t, []struct {
			name string
			tt   interface{ ToolTip() string }
		}{
			{"Force decrypt", a.forceDecryptCheck},
			{"Verify first", a.verifyFirstCheck},
			{"Delete volume", a.deleteVolumeCheck},
			{"Auto unzip", a.autoUnzipCheck},
			{"Same level", a.sameLevelCheck},
		})
	})
}
