Introduce StateMigrateJSON method/command
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9bb720f..f970942 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -148,6 +148,7 @@
           - "1.8.5"
           - "1.9.8"
           - "1.10.4"
+          - "1.16.2"
           - "refs/heads/main"
     steps:
       -
diff --git a/tfexec/internal/e2etest/state_migrate_test.go b/tfexec/internal/e2etest/state_migrate_test.go
new file mode 100644
index 0000000..e32e1b3
--- /dev/null
+++ b/tfexec/internal/e2etest/state_migrate_test.go
@@ -0,0 +1,30 @@
+// Copyright IBM Corp. 2020, 2026
+// SPDX-License-Identifier: MPL-2.0
+
+package e2etest
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/go-version"
+
+	"github.com/hashicorp/terraform-exec/tfexec"
+)
+
+func TestStateMigrateJSON(t *testing.T) {
+	runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(stateMigrateMinVersion) {
+			t.Skip("state migrate command not available in this Terraform version")
+		}
+
+		// TODO: backend local(path foo) -> backend local(path boo)
+		err := tf.StateMigrateJSON(context.Background())
+		if err != nil {
+			t.Fatalf("error running StateMigrateJSON: %s", err)
+		}
+	})
+
+	// TODO: upgrade test? Is there a provider to test with?
+	// TODO: state store test? Is there a provider to test with?
+}
diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go
index 7fd872c..0b82d47 100644
--- a/tfexec/internal/e2etest/util_test.go
+++ b/tfexec/internal/e2etest/util_test.go
@@ -29,6 +29,10 @@
 	providerAddressMinVersion = version.Must(version.NewVersion("0.13.0"))
 
 	metadataFunctionsMinVersion = version.Must(version.NewVersion("1.4.0"))
+
+	// TODO: Confirm how complete the implementation of that command actually is in this version
+	// it might be enough for the purpose of the E2E test
+	stateMigrateMinVersion = version.Must(version.NewVersion("1.16.0"))
 )
 
 func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) {
diff --git a/tfexec/options.go b/tfexec/options.go
index 30d6b6a..b132f60 100644
--- a/tfexec/options.go
+++ b/tfexec/options.go
@@ -476,3 +476,19 @@
 func LockFile(useLockFile bool) *LockFileOption {
 	return &LockFileOption{useLockFile: useLockFile}
 }
+
+type SourceProviderLockFileOption struct {
+	path string
+}
+
+func SourceProviderLockFile(path string) *SourceProviderLockFileOption {
+	return &SourceProviderLockFileOption{path: path}
+}
+
+type DestinationProviderLockFileOption struct {
+	path string
+}
+
+func DestinationProviderLockFile(path string) *DestinationProviderLockFileOption {
+	return &DestinationProviderLockFileOption{path: path}
+}
diff --git a/tfexec/state_migrate.go b/tfexec/state_migrate.go
new file mode 100644
index 0000000..72d3e60
--- /dev/null
+++ b/tfexec/state_migrate.go
@@ -0,0 +1,84 @@
+// Copyright IBM Corp. 2020, 2026
+// SPDX-License-Identifier: MPL-2.0
+
+package tfexec
+
+import (
+	"context"
+	"os/exec"
+)
+
+const defaultLockFilePath = ".terraform.lock.hcl"
+
+type stateMigrateConfig struct {
+	srcProviderLockFile string
+	dstProviderLockFile string
+
+	upgrade bool
+}
+
+var defaultStateMigrateOptions = stateMigrateConfig{
+	srcProviderLockFile: defaultLockFilePath,
+	dstProviderLockFile: defaultLockFilePath,
+}
+
+// StateMigrateOption represents options used in the Refresh method.
+type StateMigrateOption interface {
+	configureStateMigrate(*stateMigrateConfig)
+}
+
+func (opt *SourceProviderLockFileOption) configureStateMigrate(conf *stateMigrateConfig) {
+	conf.srcProviderLockFile = opt.path
+}
+
+func (opt *DestinationProviderLockFileOption) configureStateMigrate(conf *stateMigrateConfig) {
+	conf.dstProviderLockFile = opt.path
+}
+
+func (opt *UpgradeOption) configureStateMigrate(conf *stateMigrateConfig) {
+	conf.upgrade = opt.upgrade
+}
+
+// StateMigrateJSON executes `terraform state migrate` with `-json` flag
+// and any specified options and waits for it to complete.
+//
+// StateMigrateJSON is likely to be removed in a future major version in favour of
+// StateMigrate returning JSON by default.
+func (tf *Terraform) StateMigrateJSON(ctx context.Context, opts ...StateMigrateOption) error {
+	cmd, err := tf.stateMigrateJSONCmd(ctx, opts...)
+	if err != nil {
+		return err
+	}
+
+	// TODO: JSON parsing
+	// TODO: error handling (stderr and parsing?)
+
+	return tf.runTerraformCmd(ctx, cmd)
+}
+
+func (tf *Terraform) stateMigrateJSONCmd(ctx context.Context, opts ...StateMigrateOption) (*exec.Cmd, error) {
+	c := defaultStateMigrateOptions
+
+	for _, o := range opts {
+		o.configureStateMigrate(&c)
+	}
+
+	args := tf.buildStateMigrateArgs(c)
+
+	return tf.buildTerraformCmd(ctx, nil, args...), nil
+}
+
+func (tf *Terraform) buildStateMigrateArgs(c stateMigrateConfig) []string {
+	args := []string{"state", "migrate", "-no-color", "-input=false", "-json"}
+
+	if c.srcProviderLockFile != "" {
+		args = append(args, "-source-provider-lock-file="+c.srcProviderLockFile)
+	}
+	if c.dstProviderLockFile != "" {
+		args = append(args, "-destination-provider-lock-file="+c.dstProviderLockFile)
+	}
+	if c.upgrade {
+		args = append(args, "-upgrade")
+	}
+	return args
+}
diff --git a/tfexec/state_migrate_test.go b/tfexec/state_migrate_test.go
new file mode 100644
index 0000000..24b09da
--- /dev/null
+++ b/tfexec/state_migrate_test.go
@@ -0,0 +1,61 @@
+// Copyright IBM Corp. 2020, 2026
+// SPDX-License-Identifier: MPL-2.0
+
+package tfexec
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestStateMigrateJSONCmd(t *testing.T) {
+	td := t.TempDir()
+
+	tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// empty env, to avoid environ mismatch in testing
+	tf.SetEnv(map[string]string{})
+
+	t.Run("defaults", func(t *testing.T) {
+		stateMigrateJSONCmd, err := tf.stateMigrateJSONCmd(context.Background())
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"state",
+			"migrate",
+			"-no-color",
+			"-input=false",
+			"-json",
+			"-source-provider-lock-file=.terraform.lock.hcl",
+			"-destination-provider-lock-file=.terraform.lock.hcl",
+		}, nil, stateMigrateJSONCmd)
+	})
+
+	t.Run("override all defaults", func(t *testing.T) {
+		stateMigrateJSONCmd, err := tf.stateMigrateJSONCmd(context.Background(),
+			SourceProviderLockFile("src.lock.hcl"),
+			DestinationProviderLockFile("dst.lock.hcl"),
+			Upgrade(true))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"state",
+			"migrate",
+			"-no-color",
+			"-input=false",
+			"-json",
+			"-source-provider-lock-file=src.lock.hcl",
+			"-destination-provider-lock-file=dst.lock.hcl",
+			"-upgrade",
+		}, nil, stateMigrateJSONCmd)
+	})
+}