package main

import (
	"bufio"
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/steveyegge/beads/internal/types"
)



func TestExportCommand(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "bd-test-export-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	testDB := filepath.Join(tmpDir, "test.db")
	s := newTestStore(t, testDB)
	defer s.Close()

	ctx := context.Background()

	// Create test issues
	issues := []*types.Issue{
		{
			Title:       "First Issue",
			Description: "Test description 1",
			Priority:    0,
			IssueType:   types.TypeBug,
			Status:      types.StatusOpen,
		},
		{
			Title:       "Second Issue",
			Description: "Test description 2",
			Priority:    1,
			IssueType:   types.TypeFeature,
			Status:      types.StatusInProgress,
		},
	}

	for _, issue := range issues {
		if err := s.CreateIssue(ctx, issue, "test-user"); err != nil {
			t.Fatalf("Failed to create issue: %v", err)
		}
	}

	// Add a label to first issue
	if err := s.AddLabel(ctx, issues[0].ID, "critical", "test-user"); err != nil {
		t.Fatalf("Failed to add label: %v", err)
	}

	// Add a dependency
	dep := &types.Dependency{
		IssueID:     issues[0].ID,
		DependsOnID: issues[1].ID,
		Type:        "blocks",
	}
	if err := s.AddDependency(ctx, dep, "test-user"); err != nil {
		t.Fatalf("Failed to add dependency: %v", err)
	}

	t.Run("export to file", func(t *testing.T) {
		exportPath := filepath.Join(tmpDir, "export.jsonl")

		// Set up global state
		store = s
		dbPath = testDB
		rootCtx = ctx
		defer func() { rootCtx = nil }()

		// Create a mock command with output flag
		exportCmd.SetArgs([]string{"-o", exportPath})
		exportCmd.Flags().Set("output", exportPath)

		// Export
		exportCmd.Run(exportCmd, []string{})

		// Verify file was created
		if _, err := os.Stat(exportPath); os.IsNotExist(err) {
			t.Fatal("Export file was not created")
		}

		// Read and verify JSONL content
		file, err := os.Open(exportPath)
		if err != nil {
			t.Fatalf("Failed to open export file: %v", err)
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		lineCount := 0
		for scanner.Scan() {
			lineCount++
			var issue types.Issue
			if err := json.Unmarshal(scanner.Bytes(), &issue); err != nil {
				t.Fatalf("Failed to parse JSONL line %d: %v", lineCount, err)
			}

			// Verify issue has required fields
			if issue.ID == "" {
				t.Error("Issue missing ID")
			}
			if issue.Title == "" {
				t.Error("Issue missing title")
			}
		}

		if lineCount != 2 {
			t.Errorf("Expected 2 lines in export, got %d", lineCount)
		}
	})

	t.Run("export includes labels", func(t *testing.T) {
		exportPath := filepath.Join(tmpDir, "export_labels.jsonl")

		// Clear export hashes to force re-export (test isolation)
		if err := s.ClearAllExportHashes(ctx); err != nil {
			t.Fatalf("Failed to clear export hashes: %v", err)
		}

		store = s
		dbPath = testDB
		rootCtx = ctx
		defer func() { rootCtx = nil }()
		exportCmd.Flags().Set("output", exportPath)
		exportCmd.Run(exportCmd, []string{})

		file, err := os.Open(exportPath)
		if err != nil {
			t.Fatalf("Failed to open export file: %v", err)
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		foundLabeledIssue := false
		for scanner.Scan() {
			var issue types.Issue
			if err := json.Unmarshal(scanner.Bytes(), &issue); err != nil {
				t.Fatalf("Failed to parse JSONL: %v", err)
			}

			if issue.ID == issues[0].ID {
				foundLabeledIssue = true
				if len(issue.Labels) != 1 || issue.Labels[0] != "critical" {
					t.Errorf("Expected label 'critical', got %v", issue.Labels)
				}
			}
		}

		if !foundLabeledIssue {
			t.Error("Did not find labeled issue in export")
		}
	})

	t.Run("export includes dependencies", func(t *testing.T) {
		exportPath := filepath.Join(tmpDir, "export_deps.jsonl")

		// Clear export hashes to force re-export (test isolation)
		if err := s.ClearAllExportHashes(ctx); err != nil {
			t.Fatalf("Failed to clear export hashes: %v", err)
		}

		store = s
		dbPath = testDB
		rootCtx = ctx
		defer func() { rootCtx = nil }()
		exportCmd.Flags().Set("output", exportPath)
		exportCmd.Run(exportCmd, []string{})

		file, err := os.Open(exportPath)
		if err != nil {
			t.Fatalf("Failed to open export file: %v", err)
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		foundDependency := false
		for scanner.Scan() {
			var issue types.Issue
			if err := json.Unmarshal(scanner.Bytes(), &issue); err != nil {
				t.Fatalf("Failed to parse JSONL: %v", err)
			}

			if issue.ID == issues[0].ID && len(issue.Dependencies) > 0 {
				foundDependency = true
				if issue.Dependencies[0].DependsOnID != issues[1].ID {
					t.Errorf("Expected dependency to %s, got %s", issues[1].ID, issue.Dependencies[0].DependsOnID)
				}
			}
		}

		if !foundDependency {
			t.Error("Did not find dependency in export")
		}
	})

	t.Run("validate export path", func(t *testing.T) {
		// Test safe path
		if err := validateExportPath(tmpDir); err != nil {
			t.Errorf("Unexpected error for safe path: %v", err)
		}

		// Test Windows system directories
		// Note: validateExportPath() only checks Windows paths on case-insensitive systems
		// On Unix/Mac, C:\Windows won't match, so we skip this assertion
		// Just verify the function doesn't panic with Windows-style paths
		_ = validateExportPath("C:\\Windows\\system32\\test.jsonl")
	})

	t.Run("prevent exporting empty database over non-empty JSONL", func(t *testing.T) {
		exportPath := filepath.Join(tmpDir, "export_empty_check.jsonl")

		// First, create a JSONL file with issues
		file, err := os.Create(exportPath)
		if err != nil {
			t.Fatalf("Failed to create JSONL: %v", err)
		}
		encoder := json.NewEncoder(file)
		for _, issue := range issues {
			if err := encoder.Encode(issue); err != nil {
				t.Fatalf("Failed to encode issue: %v", err)
			}
		}
		file.Close()

		// Verify file has issues
		count, err := countIssuesInJSONL(exportPath)
		if err != nil {
			t.Fatalf("Failed to count issues: %v", err)
		}
		if count != 2 {
			t.Fatalf("Expected 2 issues in JSONL, got %d", count)
		}

		// Create empty database
		emptyDBPath := filepath.Join(tmpDir, "empty.db")
		emptyStore := newTestStore(t, emptyDBPath)
		defer emptyStore.Close()

		// Test using exportToJSONLWithStore directly (daemon code path)
		err = exportToJSONLWithStore(ctx, emptyStore, exportPath)
		if err == nil {
			t.Error("Expected error when exporting empty database over non-empty JSONL")
		} else {
			expectedMsg := "refusing to export empty database over non-empty JSONL file (database: 0 issues, JSONL: 2 issues). This would result in data loss"
			if err.Error() != expectedMsg {
				t.Errorf("Unexpected error message:\nGot:      %q\nExpected: %q", err.Error(), expectedMsg)
			}
		}

		// Verify JSONL file is unchanged
		countAfter, err := countIssuesInJSONL(exportPath)
		if err != nil {
			t.Fatalf("Failed to count issues after failed export: %v", err)
		}
		if countAfter != 2 {
			t.Errorf("JSONL file was modified! Expected 2 issues, got %d", countAfter)
		}
	})

	t.Run("verify JSONL line count matches exported count", func(t *testing.T) {
		exportPath := filepath.Join(tmpDir, "export_verify.jsonl")

		// Clear export hashes to force re-export
		if err := s.ClearAllExportHashes(ctx); err != nil {
			t.Fatalf("Failed to clear export hashes: %v", err)
		}

		store = s
		dbPath = testDB
		rootCtx = ctx
		defer func() { rootCtx = nil }()
		exportCmd.Flags().Set("output", exportPath)
		exportCmd.Run(exportCmd, []string{})

		// Verify the exported file has exactly 2 lines
		actualCount, err := countIssuesInJSONL(exportPath)
		if err != nil {
			t.Fatalf("Failed to count issues in JSONL: %v", err)
		}
		if actualCount != 2 {
			t.Errorf("Expected 2 issues in JSONL, got %d", actualCount)
		}

		// Simulate corrupted export by truncating file
		corruptedPath := filepath.Join(tmpDir, "export_corrupted.jsonl")
		
		// First export normally
		if err := s.ClearAllExportHashes(ctx); err != nil {
			t.Fatalf("Failed to clear export hashes: %v", err)
		}
		store = s
		rootCtx = ctx
		defer func() { rootCtx = nil }()
		exportCmd.Flags().Set("output", corruptedPath)
		exportCmd.Run(exportCmd, []string{})

		// Now manually corrupt it by removing one line
		file, err := os.Open(corruptedPath)
		if err != nil {
			t.Fatalf("Failed to open file for corruption: %v", err)
		}
		scanner := bufio.NewScanner(file)
		var lines []string
		for scanner.Scan() {
			lines = append(lines, scanner.Text())
		}
		file.Close()

		// Write back only first line (simulating partial write)
		corruptedFile, err := os.Create(corruptedPath)
		if err != nil {
			t.Fatalf("Failed to create corrupted file: %v", err)
		}
		corruptedFile.WriteString(lines[0] + "\n")
		corruptedFile.Close()

		// Verify countIssuesInJSONL detects the corruption
		count, err := countIssuesInJSONL(corruptedPath)
		if err != nil {
			t.Fatalf("Failed to count corrupted file: %v", err)
		}
		if count != 1 {
			t.Errorf("Expected 1 line in corrupted file, got %d", count)
		}
	})

	t.Run("export cancellation", func(t *testing.T) {
		// Create a large number of issues to ensure export takes time
		ctx := context.Background()
		largeStore := newTestStore(t, filepath.Join(tmpDir, "large.db"))
		defer largeStore.Close()

		// Create 100 issues
		for i := 0; i < 100; i++ {
			issue := &types.Issue{
				Title:       "Test Issue",
				Description: "Test description for cancellation",
				Priority:    0,
				IssueType:   types.TypeBug,
				Status:      types.StatusOpen,
			}
			if err := largeStore.CreateIssue(ctx, issue, "test-user"); err != nil {
				t.Fatalf("Failed to create issue: %v", err)
			}
		}

		exportPath := filepath.Join(tmpDir, "export_cancel.jsonl")

		// Create a cancellable context
		cancelCtx, cancel := context.WithCancel(context.Background())

		// Start export in a goroutine
		errChan := make(chan error, 1)
		go func() {
			errChan <- exportToJSONLWithStore(cancelCtx, largeStore, exportPath)
		}()

		// Cancel after a short delay
		cancel()

		// Wait for export to finish
		err := <-errChan

		// Verify that the operation was cancelled
		if err != nil && err != context.Canceled {
			t.Logf("Export returned error: %v (expected context.Canceled)", err)
		}

		// Verify database integrity - we should still be able to query
		issues, err := largeStore.SearchIssues(ctx, "", types.IssueFilter{})
		if err != nil {
			t.Fatalf("Database corrupted after cancellation: %v", err)
		}
		if len(issues) != 100 {
			t.Errorf("Expected 100 issues after cancellation, got %d", len(issues))
		}
	})
}
