diff --git a/internal/cmd/output.go b/internal/cmd/output.go
new file mode 100644
index 0000000..e566347
--- /dev/null
+++ b/internal/cmd/output.go
@@ -0,0 +1,208 @@
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// displayTableStats displays stats in a table format
+func displayTableStats(stats *StatsCollector) {
+	// ANSI color helpers
+	green := "\033[32m"
+	yellow := "\033[33m"
+	reset := "\033[0m"
+	colorize := func(s, color string) string {
+		if noColor {
+			return s
+		}
+		return color + s + reset
+	}
+
+	// Find max repo name length
+	maxRepoLen := len("Repository")
+	for _, repoStat := range stats.PerRepoStats {
+		if l := len(repoStat.RepoName); l > maxRepoLen {
+			maxRepoLen = l
+		}
+	}
+	if maxRepoLen > 40 {
+		maxRepoLen = 40 // hard cap for very long repo names
+	}
+
+	repoCol := maxRepoLen
+	colWidths := []int{repoCol, 14, 20, 12}
+
+	// Table border helpers
+	top := "╭"
+	sep := "├"
+	bot := "╰"
+	for i, w := range colWidths {
+		top += pad("─", w+2) // +2 for padding spaces
+		sep += pad("─", w+2)
+		bot += pad("─", w+2)
+		if i < len(colWidths)-1 {
+			top += "┬"
+			sep += "┼"
+			bot += "┴"
+		} else {
+			top += "╮"
+			sep += "┤"
+			bot += "╯"
+		}
+	}
+
+	headRepo := fmt.Sprintf("%-*s", repoCol, "Repository")
+	headCombined := fmt.Sprintf("%*s", colWidths[1], "PRs Combined")
+	headSkipped := fmt.Sprintf("%-*s", colWidths[2], "Skipped")
+	headStatus := fmt.Sprintf("%-*s", colWidths[3], "Status")
+	head := fmt.Sprintf(
+		"│ %-*s │ %s │ %s │ %s │",
+		repoCol, headRepo,
+		headCombined,
+		headSkipped,
+		headStatus,
+	)
+
+	fmt.Println(top)
+	fmt.Println(head)
+	fmt.Println(sep)
+
+	for _, repoStat := range stats.PerRepoStats {
+		status := "OK"
+		statusColor := green
+		if repoStat.TotalPRs == 0 {
+			status = "NO OPEN PRs"
+			statusColor = green
+		} else if repoStat.NotEnoughPRs {
+			status = "NOT ENOUGH"
+			statusColor = yellow
+		}
+
+		mcColor := green
+		dnmColor := green
+		if repoStat.SkippedMergeConf > 0 {
+			mcColor = yellow
+		}
+		if repoStat.SkippedCriteria > 0 {
+			dnmColor = yellow
+		}
+		mcRaw := fmt.Sprintf("%d", repoStat.SkippedMergeConf)
+		dnmRaw := fmt.Sprintf("%d", repoStat.SkippedCriteria)
+		skippedRaw := fmt.Sprintf("%s (MC), %s (DNM)", mcRaw, dnmRaw)
+		skippedPadded := fmt.Sprintf("%-*s", colWidths[2], skippedRaw)
+		mcIdx := strings.Index(skippedPadded, mcRaw)
+		dnmIdx := strings.Index(skippedPadded, dnmRaw)
+		skippedColored := skippedPadded
+		if mcIdx != -1 {
+			skippedColored = skippedColored[:mcIdx] + colorize(mcRaw, mcColor) + skippedColored[mcIdx+len(mcRaw):]
+		}
+		if dnmIdx != -1 {
+			dnmIdx = strings.Index(skippedColored, dnmRaw)
+			skippedColored = skippedColored[:dnmIdx] + colorize(dnmRaw, dnmColor) + skippedColored[dnmIdx+len(dnmRaw):]
+		}
+		statusColored := colorize(status, statusColor)
+		statusColored = fmt.Sprintf("%-*s", colWidths[3]+len(statusColored)-len(status), statusColored)
+
+		fmt.Printf(
+			"│ %-*s │ %s │ %s │ %s │\n",
+			repoCol, repoStat.RepoName,
+			fmt.Sprintf("%*d", colWidths[1], repoStat.CombinedCount),
+			skippedColored,
+			statusColored,
+		)
+	}
+	fmt.Println(bot)
+
+	// Print summary mini-table with proper padding
+	summaryTop := "╭───────────────┬───────────────┬───────────────────────┬───────────────╮"
+	summaryHead := "│ Repos         │ Combined PRs  │ Skipped               │ Total PRs     │"
+	summarySep := "├───────────────┼───────────────┼───────────────────────┼───────────────┤"
+	skippedRaw := fmt.Sprintf("%d (MC), %d (DNM)", stats.PRsSkippedMergeConflict, stats.PRsSkippedCriteria)
+	summaryRow := fmt.Sprintf(
+		"│ %-13d │ %-13d │ %-21s │ %-13d │",
+		stats.ReposProcessed,
+		stats.PRsCombined,
+		skippedRaw,
+		len(stats.CombinedPRLinks),
+	)
+	summaryBot := "╰───────────────┴───────────────┴───────────────────────┴───────────────╯"
+	fmt.Println()
+	fmt.Println(summaryTop)
+	fmt.Println(summaryHead)
+	fmt.Println(summarySep)
+	fmt.Println(summaryRow)
+	fmt.Println(summaryBot)
+
+	// Print PR links block (blue color)
+	if len(stats.CombinedPRLinks) > 0 {
+		blue := "\033[34m"
+		fmt.Println("\nLinks to Combined PRs:")
+		for _, link := range stats.CombinedPRLinks {
+			if noColor {
+				fmt.Println("-", link)
+			} else {
+				fmt.Printf("- %s%s%s\n", blue, link, reset)
+			}
+		}
+	}
+	fmt.Println()
+}
+
+// displayJSONStats displays stats in JSON format
+func displayJSONStats(stats *StatsCollector) {
+	output := map[string]interface{}{
+		"reposProcessed":          stats.ReposProcessed,
+		"prsCombined":             stats.PRsCombined,
+		"prsSkippedMergeConflict": stats.PRsSkippedMergeConflict,
+		"prsSkippedCriteria":      stats.PRsSkippedCriteria,
+		"executionTime":           stats.EndTime.Sub(stats.StartTime).String(),
+		"combinedPRLinks":         stats.CombinedPRLinks,
+		"perRepoStats":            stats.PerRepoStats,
+	}
+	jsonData, _ := json.MarshalIndent(output, "", "  ")
+	fmt.Println(string(jsonData))
+}
+
+// displayPlainStats displays stats in plain text format
+func displayPlainStats(stats *StatsCollector) {
+	elapsed := stats.EndTime.Sub(stats.StartTime)
+	fmt.Printf("Repositories Processed: %d\n", stats.ReposProcessed)
+	fmt.Printf("PRs Combined: %d\n", stats.PRsCombined)
+	fmt.Printf("PRs Skipped (Merge Conflicts): %d\n", stats.PRsSkippedMergeConflict)
+	fmt.Printf("PRs Skipped (Did Not Match): %d\n", stats.PRsSkippedCriteria)
+	fmt.Printf("Execution Time: %s\n", elapsed.Round(time.Second))
+
+	fmt.Println("Links to Combined PRs:")
+	for _, link := range stats.CombinedPRLinks {
+		fmt.Println("-", link)
+	}
+
+	fmt.Println("\nPer-Repository Details:")
+	for _, repoStat := range stats.PerRepoStats {
+		fmt.Printf("  %s\n", repoStat.RepoName)
+		if repoStat.NotEnoughPRs {
+			fmt.Println("    Not enough PRs to combine.")
+			continue
+		}
+		fmt.Printf("    Combined: %d\n", repoStat.CombinedCount)
+		fmt.Printf("    Skipped (Merge Conflicts): %d\n", repoStat.SkippedMergeConf)
+		fmt.Printf("    Skipped (Did Not Match): %d\n", repoStat.SkippedCriteria)
+		if repoStat.CombinedPRLink != "" {
+			fmt.Printf("    Combined PR: %s\n", repoStat.CombinedPRLink)
+		}
+	}
+}
+
+// pad returns a string of n runes of s (usually "─")
+func pad(s string, n int) string {
+	if n <= 0 {
+		return ""
+	}
+	out := ""
+	for i := 0; i < n; i++ {
+		out += s
+	}
+	return out
+}
diff --git a/internal/cmd/output_test.go b/internal/cmd/output_test.go
new file mode 100644
index 0000000..7dc640a
--- /dev/null
+++ b/internal/cmd/output_test.go
@@ -0,0 +1,111 @@
+package cmd
+
+import (
+	"testing"
+	"time"
+)
+
+func TestDisplayTableStats(t *testing.T) {
+	stats := &StatsCollector{
+		ReposProcessed:          2,
+		PRsCombined:             5,
+		PRsSkippedMergeConflict: 1,
+		PRsSkippedCriteria:      2,
+		CombinedPRLinks:         []string{"http://example.com/pr1", "http://example.com/pr2"},
+		PerRepoStats: map[string]*RepoStats{
+			"repo1": {
+				RepoName:         "repo1",
+				CombinedCount:    3,
+				SkippedMergeConf: 1,
+				SkippedCriteria:  0,
+				CombinedPRLink:   "http://example.com/pr1",
+				NotEnoughPRs:     false,
+				TotalPRs:         5,
+			},
+			"repo2": {
+				RepoName:         "repo2",
+				CombinedCount:    2,
+				SkippedMergeConf: 0,
+				SkippedCriteria:  2,
+				CombinedPRLink:   "http://example.com/pr2",
+				NotEnoughPRs:     false,
+				TotalPRs:         4,
+			},
+		},
+		StartTime: time.Now(),
+		EndTime:   time.Now().Add(2 * time.Minute),
+	}
+
+	displayTableStats(stats)
+	// Add assertions or manual verification as needed
+}
+
+func TestDisplayJSONStats(t *testing.T) {
+	stats := &StatsCollector{
+		ReposProcessed:          2,
+		PRsCombined:             5,
+		PRsSkippedMergeConflict: 1,
+		PRsSkippedCriteria:      2,
+		CombinedPRLinks:         []string{"http://example.com/pr1", "http://example.com/pr2"},
+		PerRepoStats: map[string]*RepoStats{
+			"repo1": {
+				RepoName:         "repo1",
+				CombinedCount:    3,
+				SkippedMergeConf: 1,
+				SkippedCriteria:  0,
+				CombinedPRLink:   "http://example.com/pr1",
+				NotEnoughPRs:     false,
+				TotalPRs:         5,
+			},
+			"repo2": {
+				RepoName:         "repo2",
+				CombinedCount:    2,
+				SkippedMergeConf: 0,
+				SkippedCriteria:  2,
+				CombinedPRLink:   "http://example.com/pr2",
+				NotEnoughPRs:     false,
+				TotalPRs:         4,
+			},
+		},
+		StartTime: time.Now(),
+		EndTime:   time.Now().Add(2 * time.Minute),
+	}
+
+	displayJSONStats(stats)
+	// Add assertions or manual verification as needed
+}
+
+func TestDisplayPlainStats(t *testing.T) {
+	stats := &StatsCollector{
+		ReposProcessed:          2,
+		PRsCombined:             5,
+		PRsSkippedMergeConflict: 1,
+		PRsSkippedCriteria:      2,
+		CombinedPRLinks:         []string{"http://example.com/pr1", "http://example.com/pr2"},
+		PerRepoStats: map[string]*RepoStats{
+			"repo1": {
+				RepoName:         "repo1",
+				CombinedCount:    3,
+				SkippedMergeConf: 1,
+				SkippedCriteria:  0,
+				CombinedPRLink:   "http://example.com/pr1",
+				NotEnoughPRs:     false,
+				TotalPRs:         5,
+			},
+			"repo2": {
+				RepoName:         "repo2",
+				CombinedCount:    2,
+				SkippedMergeConf: 0,
+				SkippedCriteria:  2,
+				CombinedPRLink:   "http://example.com/pr2",
+				NotEnoughPRs:     false,
+				TotalPRs:         4,
+			},
+		},
+		StartTime: time.Now(),
+		EndTime:   time.Now().Add(2 * time.Minute),
+	}
+
+	displayPlainStats(stats)
+	// Add assertions or manual verification as needed
+}
diff --git a/internal/cmd/root.go b/internal/cmd/root.go
index 206a492..72dc470 100644
--- a/internal/cmd/root.go
+++ b/internal/cmd/root.go
@@ -2,10 +2,8 @@ package cmd
 
 import (
 	"context"
-	"encoding/json"
 	"errors"
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/cli/go-gh/v2/pkg/api"
@@ -422,218 +420,3 @@ func displayStatsSummary(stats *StatsCollector, outputFormat string) {
 		displayPlainStats(stats)
 	}
 }
-
-func displayTableStats(stats *StatsCollector) {
-	// ANSI color helpers
-	green := "\033[32m"
-	yellow := "\033[33m"
-	reset := "\033[0m"
-	colorize := func(s, color string) string {
-		if noColor {
-			return s
-		}
-		return color + s + reset
-	}
-
-	// Find max repo name length
-	maxRepoLen := len("Repository")
-	for _, repoStat := range stats.PerRepoStats {
-		if l := len(repoStat.RepoName); l > maxRepoLen {
-			maxRepoLen = l
-		}
-	}
-	if maxRepoLen > 40 {
-		maxRepoLen = 40 // hard cap for very long repo names
-	}
-
-	repoCol := maxRepoLen
-	colWidths := []int{repoCol, 14, 20, 12}
-
-	// Table border helpers
-	top := "╭"
-	sep := "├"
-	bot := "╰"
-	for i, w := range colWidths {
-		top += pad("─", w+2) // +2 for padding spaces
-		sep += pad("─", w+2)
-		bot += pad("─", w+2)
-		if i < len(colWidths)-1 {
-			top += "┬"
-			sep += "┼"
-			bot += "┴"
-		} else {
-			top += "╮"
-			sep += "┤"
-			bot += "╯"
-		}
-	}
-
-	headRepo := fmt.Sprintf("%-*s", repoCol, "Repository")
-	headCombined := fmt.Sprintf("%*s", colWidths[1], "PRs Combined")
-	headSkipped := fmt.Sprintf("%-*s", colWidths[2], "Skipped")
-	headStatus := fmt.Sprintf("%-*s", colWidths[3], "Status")
-	head := fmt.Sprintf(
-		"│ %-*s │ %s │ %s │ %s │",
-		repoCol, headRepo,
-		headCombined,
-		headSkipped,
-		headStatus,
-	)
-
-	fmt.Println(top)
-	fmt.Println(head)
-	fmt.Println(sep)
-
-	for _, repoStat := range stats.PerRepoStats {
-		status := "OK"
-		statusColor := green
-		if repoStat.TotalPRs == 0 {
-			status = "NO OPEN PRs"
-			statusColor = green
-		} else if repoStat.NotEnoughPRs {
-			status = "NOT ENOUGH"
-			statusColor = yellow
-		}
-
-		mcColor := green
-		dnmColor := green
-		if repoStat.SkippedMergeConf > 0 {
-			mcColor = yellow
-		}
-		if repoStat.SkippedCriteria > 0 {
-			dnmColor = yellow
-		}
-		mcRaw := fmt.Sprintf("%d", repoStat.SkippedMergeConf)
-		dnmRaw := fmt.Sprintf("%d", repoStat.SkippedCriteria)
-		skippedRaw := fmt.Sprintf("%s (MC), %s (DNM)", mcRaw, dnmRaw)
-
-		repoName := truncate(repoStat.RepoName, repoCol)
-		combined := fmt.Sprintf("%*d", colWidths[1], repoStat.CombinedCount)
-		// Pad skippedRaw to colWidths[2] before coloring
-		skippedPadded := fmt.Sprintf("%-*s", colWidths[2], skippedRaw)
-		// Colorize only the numbers in the padded string
-		mcIdx := strings.Index(skippedPadded, mcRaw)
-		dnmIdx := strings.Index(skippedPadded, dnmRaw)
-		skippedColored := skippedPadded
-		if mcIdx != -1 {
-			skippedColored = skippedColored[:mcIdx] + colorize(mcRaw, mcColor) + skippedColored[mcIdx+len(mcRaw):]
-		}
-		if dnmIdx != -1 {
-			dnmIdx = strings.Index(skippedColored, dnmRaw) // recalc in case mcRaw and dnmRaw overlap
-			skippedColored = skippedColored[:dnmIdx] + colorize(dnmRaw, dnmColor) + skippedColored[dnmIdx+len(dnmRaw):]
-		}
-		statusColored := colorize(status, statusColor)
-		statusColored = fmt.Sprintf("%-*s", colWidths[3]+len(statusColored)-len(status), statusColored)
-
-		fmt.Printf(
-			"│ %-*s │ %s │ %s │ %s │\n",
-			repoCol, repoName,
-			combined,
-			skippedColored,
-			statusColored,
-		)
-	}
-	fmt.Println(bot)
-
-	// Print summary mini-table with proper padding
-	summaryTop := "╭───────────────┬───────────────┬───────────────────────┬───────────────╮"
-	summaryHead := "│ Repos         │ Combined PRs  │ Skipped               │ Total PRs     │"
-	summarySep := "├───────────────┼───────────────┼───────────────────────┼───────────────┤"
-	skippedRaw := fmt.Sprintf("%d (MC), %d (DNM)", stats.PRsSkippedMergeConflict, stats.PRsSkippedCriteria)
-	summaryRow := fmt.Sprintf(
-		"│ %-13d │ %-13d │ %-21s │ %-13d │",
-		stats.ReposProcessed,
-		stats.PRsCombined,
-		skippedRaw,
-		len(stats.CombinedPRLinks),
-	)
-	summaryBot := "╰───────────────┴───────────────┴───────────────────────┴───────────────╯"
-	fmt.Println()
-	fmt.Println(summaryTop)
-	fmt.Println(summaryHead)
-	fmt.Println(summarySep)
-	fmt.Println(summaryRow)
-	fmt.Println(summaryBot)
-
-	// Print PR links block (blue color)
-	if len(stats.CombinedPRLinks) > 0 {
-		blue := "\033[34m"
-		reset := "\033[0m"
-		fmt.Println("\nLinks to Combined PRs:")
-		for _, link := range stats.CombinedPRLinks {
-			if noColor {
-				fmt.Println("-", link)
-			} else {
-				fmt.Printf("- %s%s%s\n", blue, link, reset)
-			}
-		}
-	}
-	fmt.Println()
-}
-
-// pad returns a string of n runes of s (usually "─")
-func pad(s string, n int) string {
-	if n <= 0 {
-		return ""
-	}
-	out := ""
-	for i := 0; i < n; i++ {
-		out += s
-	}
-	return out
-}
-
-// truncate shortens a string to maxLen runes, adding … if truncated
-func truncate(s string, maxLen int) string {
-	runes := []rune(s)
-	if len(runes) <= maxLen {
-		return s
-	}
-	if maxLen <= 1 {
-		return string(runes[:maxLen])
-	}
-	return string(runes[:maxLen-1]) + "…"
-}
-
-func displayJSONStats(stats *StatsCollector) {
-	output := map[string]interface{}{
-		"reposProcessed":          stats.ReposProcessed,
-		"prsCombined":             stats.PRsCombined,
-		"prsSkippedMergeConflict": stats.PRsSkippedMergeConflict,
-		"prsSkippedCriteria":      stats.PRsSkippedCriteria,
-		"executionTime":           stats.EndTime.Sub(stats.StartTime).String(),
-		"combinedPRLinks":         stats.CombinedPRLinks,
-		"perRepoStats":            stats.PerRepoStats,
-	}
-	jsonData, _ := json.MarshalIndent(output, "", "  ")
-	fmt.Println(string(jsonData))
-}
-
-func displayPlainStats(stats *StatsCollector) {
-	elapsed := stats.EndTime.Sub(stats.StartTime)
-	fmt.Printf("Repositories Processed: %d\n", stats.ReposProcessed)
-	fmt.Printf("PRs Combined: %d\n", stats.PRsCombined)
-	fmt.Printf("PRs Skipped (Merge Conflicts): %d\n", stats.PRsSkippedMergeConflict)
-	fmt.Printf("PRs Skipped (Did Not Match): %d\n", stats.PRsSkippedCriteria)
-	fmt.Printf("Execution Time: %s\n", elapsed.Round(time.Second))
-
-	fmt.Println("Links to Combined PRs:")
-	for _, link := range stats.CombinedPRLinks {
-		fmt.Println("-", link)
-	}
-
-	fmt.Println("\nPer-Repository Details:")
-	for _, repoStat := range stats.PerRepoStats {
-		fmt.Printf("  %s\n", repoStat.RepoName)
-		if repoStat.NotEnoughPRs {
-			fmt.Println("    Not enough PRs to combine.")
-			continue
-		}
-		fmt.Printf("    Combined: %d\n", repoStat.CombinedCount)
-		fmt.Printf("    Skipped (Merge Conflicts): %d\n", repoStat.SkippedMergeConf)
-		fmt.Printf("    Skipped (Did Not Match): %d\n", repoStat.SkippedCriteria)
-		if repoStat.CombinedPRLink != "" {
-			fmt.Printf("    Combined PR: %s\n", repoStat.CombinedPRLink)
-		}
-	}
-}