package regenmerge

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

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

// TestExtractLostRegistrationsSkipsHostsAbsentFromFresh pins the fix for
// issue #472: re-injection should target only host files that exist in fresh
// (i.e., TEMPLATED-CLEAN files Apply will overwrite). NOVEL hosts (preserved
// verbatim) and PUBLISHED-ONLY-TEMPLATED hosts (deleted) must be skipped —
// the former because their AddCommand calls are already in place, the latter
// because the host file no longer exists in the staging dir at injection
// time.
func TestExtractLostRegistrationsSkipsHostsAbsentFromFresh(t *testing.T) {
	t.Parallel()

	// pub has TWO host files with AddCommand calls:
	//   - root.go (also in fresh, so injection makes sense for what's "lost")
	//   - novel.go (NOT in fresh — this is a NOVEL file). Its calls are
	//     trivially "missing" from fresh because the whole file is missing.
	// Fresh has root.go only. Without the fix, novel.go's calls get queued
	// for re-injection — but novel.go is preserved verbatim, so injection
	// would create duplicate AddCommand lines.
	pubRoot := `package cli

import "github.com/spf13/cobra"

func Execute() {
	rootCmd := &cobra.Command{Use: "x"}
	rootCmd.AddCommand(newSharedCmd(nil))
	_ = rootCmd.Execute()
}

func newSharedCmd(*rootFlags) *cobra.Command { return nil }
type rootFlags struct{}
`
	pubNovel := `package cli

import "github.com/spf13/cobra"

func registerNovels(rootCmd *cobra.Command) {
	rootCmd.AddCommand(newNovelACmd(nil))
	rootCmd.AddCommand(newNovelBCmd(nil))
}

func newNovelACmd(*rootFlags) *cobra.Command { return nil }
func newNovelBCmd(*rootFlags) *cobra.Command { return nil }
`
	freshRoot := `package cli

import "github.com/spf13/cobra"

func Execute() {
	rootCmd := &cobra.Command{Use: "x"}
	rootCmd.AddCommand(newSharedCmd(nil))
	_ = rootCmd.Execute()
}

func newSharedCmd(*rootFlags) *cobra.Command { return nil }
type rootFlags struct{}
`
	pubDir, freshDir := buildSyntheticFixture(t,
		map[string]string{
			"internal/cli/root.go":  pubRoot,
			"internal/cli/novel.go": pubNovel,
		},
		map[string]string{
			"internal/cli/root.go": freshRoot,
		})

	regs, err := extractLostRegistrations(pubDir, freshDir, nil)
	require.NoError(t, err)

	// novel.go must NOT appear in lost-registrations output, even though pub
	// has AddCommand calls there that fresh doesn't. The fix anchors injection
	// targets to "exists in fresh" only.
	for _, r := range regs {
		assert.NotEqual(t, "internal/cli/novel.go", r.HostFile,
			"NOVEL host should be skipped — its calls are preserved with the file")
	}
}

// TestApplyRewritesOwnerAttribution pins issue #471: Apply must rewrite the
// owner string in TC + NEW files copied from fresh so the merged tree carries
// the destination's attribution, not the runner-of-generate's.
func TestApplyRewritesOwnerAttribution(t *testing.T) {
	t.Parallel()

	// Pub tree: matt-van-horn owner via .printing-press.json manifest.
	pubManifest := `{"schema_version":1,"owner":"matt-van-horn","cli_name":"x-pp-cli","api_name":"x"}`
	pubRoot := `// Copyright 2026 matt-van-horn. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press. DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

func Execute() {
	rootCmd := &cobra.Command{Use: "x"}
	_ = rootCmd.Execute()
}
`
	// Fresh tree: trevin-chow owner (simulates a different operator running
	// generate with their git config).
	freshManifest := `{"schema_version":1,"owner":"trevin-chow","cli_name":"x-pp-cli","api_name":"x"}`
	freshRoot := `// Copyright 2026 trevin-chow. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press. DO NOT EDIT.
package cli

import "github.com/spf13/cobra"

func Execute() {
	rootCmd := &cobra.Command{Use: "x"}
	_ = rootCmd.Execute()
}
`
	pubGoMod := "module x.test\n\ngo 1.22\n"
	freshGoMod := "module x.test\n\ngo 1.22\n"

	stagedPubDir, freshDir := stagedSyntheticPair(t,
		map[string]string{
			".printing-press.json": pubManifest,
			"go.mod":               pubGoMod,
			"internal/cli/root.go": pubRoot,
		},
		map[string]string{
			".printing-press.json": freshManifest,
			"go.mod":               freshGoMod,
			"internal/cli/root.go": freshRoot,
		})

	report, err := Classify(stagedPubDir, freshDir, Options{Force: true})
	require.NoError(t, err)
	require.NoError(t, Apply(report, Options{Force: true}))

	merged, err := os.ReadFile(filepath.Join(stagedPubDir, "internal", "cli", "root.go"))
	require.NoError(t, err)
	assert.Contains(t, string(merged), "Copyright 2026 matt-van-horn.",
		"merged root.go should carry the destination's owner")
	assert.NotContains(t, string(merged), "trevin-chow",
		"runner's owner must be rewritten away from copied fresh files")
}

// stagedSyntheticPair builds a synthetic pub/fresh pair under the test's
// CWD prefix (so validatePathAgainstCWD passes without --force) and returns
// the staged directory paths. Equivalent to stageFixture but writes file
// contents directly rather than copying from a testdata/ fixture.
func stagedSyntheticPair(t *testing.T, pubFiles, freshFiles map[string]string) (string, string) {
	t.Helper()
	cwd, err := os.Getwd()
	require.NoError(t, err)
	root := filepath.Join(cwd, "_staged", t.Name())
	require.NoError(t, os.MkdirAll(root, 0o755))
	t.Cleanup(func() { _ = os.RemoveAll(root) })

	pubDir := filepath.Join(root, "published")
	freshDir := filepath.Join(root, "fresh")
	for _, layout := range []struct {
		dir   string
		files map[string]string
	}{
		{pubDir, pubFiles},
		{freshDir, freshFiles},
	} {
		for rel, content := range layout.files {
			full := filepath.Join(layout.dir, rel)
			require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
			require.NoError(t, os.WriteFile(full, []byte(content), 0o644))
		}
	}
	// CopyDir-style assertion via path inspection — the apply uses both
	// trees, so ensure they have unique paths under the staged root for the
	// rename-swap to land in the right place.
	require.True(t, strings.HasPrefix(pubDir, cwd))
	return pubDir, freshDir
}
