package main

import (
	"bytes"
	"go/format"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"testing"

	"gotest.tools/v3/assert"
	"gotest.tools/v3/assert/cmp"
)

func TestExtractSourceFields(t *testing.T) {
	// Create a temporary spec.go file for testing
	tempDir := t.TempDir()
	specFile := filepath.Join(tempDir, "spec.go")
	
	specContent := `package dalec

type Source struct {
	// Non-source fields
	Path string
	Includes []string
	
	// Source variant fields (should be detected)
	DockerImage *SourceDockerImage
	Git         *SourceGit
	HTTP        *SourceHTTP
	Context     *SourceContext
	Build       *SourceBuild
	Inline      *SourceInline
	
	// Other pointer fields (should be ignored)
	Generate []*SourceGenerator
	Other    *NonSourceType
}

type SourceDockerImage struct{}
type SourceGit struct{}
type SourceHTTP struct{}
type SourceContext struct{}
type SourceBuild struct{}
type SourceInline struct{}
type SourceGenerator struct{}
type NonSourceType struct{}
`

	err := os.WriteFile(specFile, []byte(specContent), 0644)
	assert.NilError(t, err)

	// Change to temp directory so the parser can find spec.go
	oldWd, err := os.Getwd()
	assert.NilError(t, err)
	defer func() {
		err := os.Chdir(oldWd)
		assert.NilError(t, err)
	}()
	
	err = os.Chdir(tempDir)
	assert.NilError(t, err)

	fields, err := extractSourceFields()
	assert.NilError(t, err)

	// Verify we found the expected source fields
	expectedFields := []SourceField{
		{Name: "Build", TypeName: "SourceBuild"},
		{Name: "Context", TypeName: "SourceContext"},
		{Name: "DockerImage", TypeName: "SourceDockerImage"},
		{Name: "Git", TypeName: "SourceGit"},
		{Name: "HTTP", TypeName: "SourceHTTP"},
		{Name: "Inline", TypeName: "SourceInline"},
	}

	assert.DeepEqual(t, fields, expectedFields)
}

func TestExtractSourceFields_NoSourceStruct(t *testing.T) {
	tempDir := t.TempDir()
	specFile := filepath.Join(tempDir, "spec.go")
	
	specContent := `package dalec

type OtherStruct struct {
	Field string
}
`

	err := os.WriteFile(specFile, []byte(specContent), 0644)
	assert.NilError(t, err)

	oldWd, err := os.Getwd()
	assert.NilError(t, err)
	defer func() {
		err := os.Chdir(oldWd)
		assert.NilError(t, err)
	}()
	
	err = os.Chdir(tempDir)
	assert.NilError(t, err)

	fields, err := extractSourceFields()
	assert.NilError(t, err)
	assert.Equal(t, len(fields), 0)
}

func TestExtractSourceFields_InvalidGoFile(t *testing.T) {
	tempDir := t.TempDir()
	specFile := filepath.Join(tempDir, "spec.go")
	
	// Invalid Go syntax
	specContent := `package dalec

type Source struct {
	invalid syntax here
}
`

	err := os.WriteFile(specFile, []byte(specContent), 0644)
	assert.NilError(t, err)

	oldWd, err := os.Getwd()
	assert.NilError(t, err)
	defer func() {
		err := os.Chdir(oldWd)
		assert.NilError(t, err)
	}()
	
	err = os.Chdir(tempDir)
	assert.NilError(t, err)

	_, err = extractSourceFields()
	assert.Check(t, err != nil)
	assert.Check(t, cmp.Contains(err.Error(), "failed to parse spec.go"))
}

func TestGenerateCode(t *testing.T) {
	fields := []SourceField{
		{Name: "Git", TypeName: "SourceGit"},
		{Name: "HTTP", TypeName: "SourceHTTP"},
		{Name: "DockerImage", TypeName: "SourceDockerImage"},
	}

	code, err := generateCode(fields)
	assert.NilError(t, err)

	// Verify the generated code is valid Go
	fset := token.NewFileSet()
	_, err = parser.ParseFile(fset, "", code, parser.ParseComments)
	assert.NilError(t, err)

	codeStr := string(code)

	// Check for expected function signatures
	assert.Check(t, cmp.Contains(codeStr, "func (s *Source) validateSourceVariants() error"))
	assert.Check(t, cmp.Contains(codeStr, "func (s *Source) toInterface() source"))

	// Check for package declaration and imports
	assert.Check(t, cmp.Contains(codeStr, "package dalec"))
	assert.Check(t, cmp.Contains(codeStr, `import (`))
	assert.Check(t, cmp.Contains(codeStr, `"fmt"`))

	// Check for generated comment
	assert.Check(t, cmp.Contains(codeStr, "Code generated by cmd/gen-source-variants. DO NOT EDIT."))

	// Check that all fields are present in validation
	for _, field := range fields {
		expectedCheck := "if s." + field.Name + " != nil {"
		assert.Check(t, cmp.Contains(codeStr, expectedCheck))
	}

	// Check that all fields are present in toInterface
	for _, field := range fields {
		expectedCase := "case s." + field.Name + " != nil:"
		expectedReturn := "return s." + field.Name
		assert.Check(t, cmp.Contains(codeStr, expectedCase))
		assert.Check(t, cmp.Contains(codeStr, expectedReturn))
	}

	// Check validation logic
	assert.Check(t, cmp.Contains(codeStr, "count := 0"))
	assert.Check(t, cmp.Contains(codeStr, "count++"))
	assert.Check(t, cmp.Contains(codeStr, `return fmt.Errorf("no non-nil source variant")`))
	assert.Check(t, cmp.Contains(codeStr, `return fmt.Errorf("more than one source variant defined")`))

	// Check toInterface logic
	assert.Check(t, cmp.Contains(codeStr, "panic(errNoSourceVariant)"))
}

func TestGenerateCode_EmptyFields(t *testing.T) {
	fields := []SourceField{}

	code, err := generateCode(fields)
	assert.NilError(t, err)

	// Verify the generated code is still valid Go
	fset := token.NewFileSet()
	_, err = parser.ParseFile(fset, "", code, parser.ParseComments)
	assert.NilError(t, err)

	codeStr := string(code)

	// Should still have the basic structure
	assert.Check(t, cmp.Contains(codeStr, "func (s *Source) validateSourceVariants() error"))
	assert.Check(t, cmp.Contains(codeStr, "func (s *Source) toInterface() source"))
	assert.Check(t, cmp.Contains(codeStr, "count := 0"))
}

func TestGenerateCode_SingleField(t *testing.T) {
	fields := []SourceField{
		{Name: "Git", TypeName: "SourceGit"},
	}

	code, err := generateCode(fields)
	assert.NilError(t, err)

	codeStr := string(code)

	// Check for the single field
	assert.Check(t, cmp.Contains(codeStr, "if s.Git != nil {"))
	assert.Check(t, cmp.Contains(codeStr, "case s.Git != nil:"))
	assert.Check(t, cmp.Contains(codeStr, "return s.Git"))
}

func TestGenerateCode_FormattingPreserved(t *testing.T) {
	fields := []SourceField{
		{Name: "Git", TypeName: "SourceGit"},
		{Name: "HTTP", TypeName: "SourceHTTP"},
	}

	code, err := generateCode(fields)
	assert.NilError(t, err)

	// Verify that the code is properly formatted by re-formatting it
	reformatted, err := format.Source(code)
	assert.NilError(t, err)

	// Should be identical since generateCode already formats
	assert.Check(t, bytes.Equal(code, reformatted))
}

func TestMain_Integration(t *testing.T) {
	// This test verifies the main function works end-to-end
	tempDir := t.TempDir()
	
	// Create a spec.go file
	specFile := filepath.Join(tempDir, "spec.go")
	specContent := `package dalec

type Source struct {
	DockerImage *SourceDockerImage
	Git         *SourceGit
	HTTP        *SourceHTTP
}

type SourceDockerImage struct{}
type SourceGit struct{}
type SourceHTTP struct{}
`

	err := os.WriteFile(specFile, []byte(specContent), 0644)
	assert.NilError(t, err)

	// Change to temp directory
	oldWd, err := os.Getwd()
	assert.NilError(t, err)
	defer func() {
		err := os.Chdir(oldWd)
		assert.NilError(t, err)
	}()
	
	err = os.Chdir(tempDir)
	assert.NilError(t, err)

	// Set up args and call main
	outputFile := filepath.Join(tempDir, "generated.go")
	oldArgs := os.Args
	defer func() { os.Args = oldArgs }()
	
	os.Args = []string{"gen-source-variants", outputFile}

	// Capture any panics or exits
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("main() panicked: %v", r)
		}
	}()

	main()

	// Verify output file was created
	_, err = os.Stat(outputFile)
	assert.NilError(t, err)

	// Verify output file contents
	content, err := os.ReadFile(outputFile)
	assert.NilError(t, err)

	contentStr := string(content)
	assert.Check(t, cmp.Contains(contentStr, "package dalec"))
	assert.Check(t, cmp.Contains(contentStr, "func (s *Source) validateSourceVariants() error"))
	assert.Check(t, cmp.Contains(contentStr, "func (s *Source) toInterface() source"))

	// Verify it's valid Go
	fset := token.NewFileSet()
	_, err = parser.ParseFile(fset, "", content, parser.ParseComments)
	assert.NilError(t, err)
}

func TestMain_InvalidArgs(t *testing.T) {
	// We can't easily test os.Exit without subprocess testing
	// Instead, let's test the logic by checking what would happen with invalid args
	
	// Test that we get the expected behavior by checking args validation directly
	oldArgs := os.Args
	defer func() { os.Args = oldArgs }()

	// Test with no arguments - this would cause main to exit
	os.Args = []string{"gen-source-variants"}
	
	// We know from reading main() that it checks len(os.Args) != 2
	// Since we can't easily test os.Exit, we'll verify the condition
	assert.Check(t, len(os.Args) != 2, "Invalid args should trigger exit condition")
}

func TestFieldSorting(t *testing.T) {
	// Test that fields are consistently sorted
	fields := []SourceField{
		{Name: "ZField", TypeName: "SourceZ"},
		{Name: "AField", TypeName: "SourceA"},
		{Name: "MField", TypeName: "SourceM"},
	}

	code, err := generateCode(fields)
	assert.NilError(t, err)

	codeStr := string(code)

	// Check that all fields are present
	assert.Check(t, cmp.Contains(codeStr, "AField"))
	assert.Check(t, cmp.Contains(codeStr, "MField"))
	assert.Check(t, cmp.Contains(codeStr, "ZField"))

	// Check that the validation logic contains all fields
	assert.Check(t, cmp.Contains(codeStr, "if s.AField != nil {"))
	assert.Check(t, cmp.Contains(codeStr, "if s.MField != nil {"))
	assert.Check(t, cmp.Contains(codeStr, "if s.ZField != nil {"))

	// Check that the toInterface logic contains all fields
	assert.Check(t, cmp.Contains(codeStr, "case s.AField != nil:"))
	assert.Check(t, cmp.Contains(codeStr, "case s.MField != nil:"))
	assert.Check(t, cmp.Contains(codeStr, "case s.ZField != nil:"))
}

