package regenmerge

import (
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestMergeIntoFreshTreePreservesValueDrift exercises a templated
// config.go const literal change — TEMPLATED-VALUE-DRIFT must preserve
// the snapshot-side literal even when decl-sets match.
func TestMergeIntoFreshTreePreservesValueDrift(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	rel := "internal/config/config.go"
	templated := func(literal string) []byte {
		return []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package config

const authPrefix = "` + literal + `"
`)
	}
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "config"), 0o755))
	require.NoError(t, os.MkdirAll(filepath.Join(fresh, "internal", "config"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(snap, rel), templated("Token "), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(fresh, rel), templated("Bearer "), 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true}))

	got, err := os.ReadFile(filepath.Join(fresh, rel))
	require.NoError(t, err)
	assert.Contains(t, string(got), `"Token "`,
		"hand-edited literal must survive merge into fresh tree")
}

// TestMergeIntoFreshTreePreservesDeclSetAdditions covers hand-authored
// functions added to a templated client file — TEMPLATED-WITH-ADDITIONS
// must preserve the snapshot.
func TestMergeIntoFreshTreePreservesDeclSetAdditions(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	rel := "internal/client/queries.go"
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "client"), 0o755))
	require.NoError(t, os.MkdirAll(filepath.Join(fresh, "internal", "client"), 0o755))

	pubBody := []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package client

const placeholderQuery = ""

func GetTransactions() string { return "transactions" }
func GetCategories() string { return "categories" }
`)
	freshBody := []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package client

const placeholderQuery = ""
`)
	require.NoError(t, os.WriteFile(filepath.Join(snap, rel), pubBody, 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(fresh, rel), freshBody, 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true}))

	got, err := os.ReadFile(filepath.Join(fresh, rel))
	require.NoError(t, err)
	assert.Contains(t, string(got), "GetTransactions", "added function survives")
	assert.Contains(t, string(got), "GetCategories", "added function survives")
}

// TestMergeIntoFreshTreeReinjectsLostAddCommand covers a hand-added
// cmd.AddCommand line in a parent-command file — LostRegistrations must
// re-inject the call into the freshly emitted host.
func TestMergeIntoFreshTreeReinjectsLostAddCommand(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "cli"), 0o755))
	require.NoError(t, os.MkdirAll(filepath.Join(fresh, "internal", "cli"), 0o755))

	rel := "internal/cli/transactions.go"
	pubBody := []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

func newTransactionsCmd(flags *rootFlags) *cobra.Command {
	cmd := &cobra.Command{}
	cmd.AddCommand(newTransactionsListCmd(flags))
	cmd.AddCommand(newHandAddedCmd(flags))
	return cmd
}
`)
	freshBody := []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package cli

func newTransactionsCmd(flags *rootFlags) *cobra.Command {
	cmd := &cobra.Command{}
	cmd.AddCommand(newTransactionsListCmd(flags))
	return cmd
}
`)
	require.NoError(t, os.WriteFile(filepath.Join(snap, rel), pubBody, 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(fresh, rel), freshBody, 0o644))
	// Pub also has a hand-written constructor file so the referent-existence
	// check passes.
	novelRel := "internal/cli/hand_added.go"
	novel := []byte(`package cli

func newHandAddedCmd(flags *rootFlags) *cobra.Command { return nil }
`)
	require.NoError(t, os.WriteFile(filepath.Join(snap, novelRel), novel, 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true}))

	got, err := os.ReadFile(filepath.Join(fresh, rel))
	require.NoError(t, err)
	assert.Contains(t, string(got), "newHandAddedCmd",
		"hand-added AddCommand must be re-injected into fresh's transactions.go")
	assert.Contains(t, string(got), "newTransactionsListCmd",
		"existing template-emitted AddCommand survives")

	// Novel constructor file preserved.
	novelGot, err := os.ReadFile(filepath.Join(fresh, novelRel))
	require.NoError(t, err)
	assert.Contains(t, string(novelGot), "newHandAddedCmd")
}

// TestMergeIntoFreshTreeMergesGoModRequires exercises R9: hand-added
// require for novel-package deps survives.
func TestMergeIntoFreshTreeMergesGoModRequires(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	pubGoMod := []byte(`module example.com/snap

go 1.22

require (
	github.com/spf13/cobra v1.8.0
	modernc.org/sqlite v1.30.0
)
`)
	freshGoMod := []byte(`module example.com/snap

go 1.22

require (
	github.com/spf13/cobra v1.8.0
)
`)
	require.NoError(t, os.WriteFile(filepath.Join(snap, "go.mod"), pubGoMod, 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(fresh, "go.mod"), freshGoMod, 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true}))

	got, err := os.ReadFile(filepath.Join(fresh, "go.mod"))
	require.NoError(t, err)
	assert.Contains(t, string(got), "modernc.org/sqlite",
		"hand-added require must survive merge into fresh's go.mod")
}

// TestMergeIntoFreshTreeSweepsNonClassifiedFiles exercises R8: README and
// other non-Go files survive merge.
func TestMergeIntoFreshTreeSweepsNonClassifiedFiles(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	require.NoError(t, os.WriteFile(filepath.Join(snap, "README.md"), []byte("hand edited\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(snap, "Makefile"), []byte("# custom\n"), 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true}))

	got, err := os.ReadFile(filepath.Join(fresh, "README.md"))
	require.NoError(t, err)
	assert.Equal(t, "hand edited\n", string(got))

	got, err = os.ReadFile(filepath.Join(fresh, "Makefile"))
	require.NoError(t, err)
	assert.Equal(t, "# custom\n", string(got))
}

// TestMergeIntoFreshTreeNovelOnlySkipsValueDrift verifies that the
// cross-spec fallback path skips heuristic preservation.
func TestMergeIntoFreshTreeNovelOnlySkipsValueDrift(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	rel := "internal/config/config.go"
	templated := func(literal string) []byte {
		return []byte(`// Copyright 2026 owner. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
package config

const authPrefix = "` + literal + `"
`)
	}
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "config"), 0o755))
	require.NoError(t, os.MkdirAll(filepath.Join(fresh, "internal", "config"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(snap, rel), templated("Token "), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(fresh, rel), templated("Bearer "), 0o644))

	// Novel hand-written file should still survive in NovelOnly mode.
	novelRel := "internal/cli/handcrafted.go"
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "cli"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(snap, novelRel), []byte("package cli\n\nfunc Handcrafted() {}\n"), 0o644))

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, MergeIntoFreshTree(snap, fresh, report, Options{Force: true, NovelOnly: true}))

	got, err := os.ReadFile(filepath.Join(fresh, rel))
	require.NoError(t, err)
	assert.Contains(t, string(got), `"Bearer "`,
		"NovelOnly mode keeps fresh's literal value (skips TEMPLATED-VALUE-DRIFT preservation)")

	novelGot, err := os.ReadFile(filepath.Join(fresh, novelRel))
	require.NoError(t, err)
	assert.Contains(t, string(novelGot), "Handcrafted",
		"NovelOnly preserves novel hand-written files")
}

// TestMergeIntoFreshTreeRefusesSnapshotSymlinks verifies symlink safety in
// the preserve path.
func TestMergeIntoFreshTreeRefusesSnapshotSymlinks(t *testing.T) {
	t.Parallel()

	snap, fresh := makeMergeFixture(t)
	rel := "internal/cli/sneaky.go"
	require.NoError(t, os.MkdirAll(filepath.Join(snap, "internal", "cli"), 0o755))
	target := filepath.Join(snap, "external.go")
	require.NoError(t, os.WriteFile(target, []byte("package cli\n"), 0o644))
	if err := os.Symlink(target, filepath.Join(snap, rel)); err != nil {
		t.Skipf("symlink unavailable: %v", err)
	}

	report, err := Classify(snap, fresh, Options{Force: true})
	require.NoError(t, err)
	err = MergeIntoFreshTree(snap, fresh, report, Options{Force: true})
	require.Error(t, err)
	assert.Contains(t, strings.ToLower(err.Error()), "symlink")
}

func makeMergeFixture(t *testing.T) (snap, fresh string) {
	t.Helper()
	dir := t.TempDir()
	snap = filepath.Join(dir, "snap")
	fresh = filepath.Join(dir, "fresh")
	require.NoError(t, os.MkdirAll(snap, 0o755))
	require.NoError(t, os.MkdirAll(fresh, 0o755))
	return snap, fresh
}
