package cmd

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"github.com/salvacybersec/keyhunter/pkg/storage"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// seedKeysDB creates a fresh SQLite database populated with three findings
// (two openai, one anthropic; one openai row marked verified) and wires
// viper + env so the keys subcommands pick the same DB up via openDBWithKey.
// A t.Cleanup resets global state so flag/viper bleed between tests is
// impossible.
func seedKeysDB(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "keys.db")

	viper.Reset()
	viper.Set("database.path", dbPath)
	t.Setenv("KEYHUNTER_PASSPHRASE", "test-pass")
	resetKeysFlags()

	db, err := storage.Open(dbPath)
	require.NoError(t, err)
	encKey, err := loadOrCreateEncKey(db, "test-pass")
	require.NoError(t, err)

	seed := []storage.Finding{
		{ProviderName: "openai", KeyValue: "sk-aaaaaaaaaaaaaaaaaaaa", KeyMasked: "sk-aaaa...aaaa", Confidence: "high", SourcePath: "a.go", LineNumber: 10},
		{ProviderName: "openai", KeyValue: "sk-bbbbbbbbbbbbbbbbbbbb", KeyMasked: "sk-bbbb...bbbb", Confidence: "medium", SourcePath: "b.go", LineNumber: 20, Verified: true, VerifyStatus: "live"},
		{ProviderName: "anthropic", KeyValue: "sk-ant-cccccccccccccccc", KeyMasked: "sk-ant-c...cccc", Confidence: "high", SourcePath: "c.go", LineNumber: 30},
	}
	for _, f := range seed {
		_, err := db.SaveFinding(f, encKey)
		require.NoError(t, err)
	}
	require.NoError(t, db.Close())

	t.Cleanup(func() {
		viper.Reset()
		resetKeysFlags()
	})
	return dbPath
}

func resetKeysFlags() {
	flagKeysUnmask = false
	flagKeysProvider = ""
	flagKeysVerified = false
	flagKeysLimit = 0
	flagKeysFormat = "json"
	flagKeysOutFile = ""
	flagKeysYes = false
}

func TestKeysList_Default(t *testing.T) {
	_ = seedKeysDB(t)
	var out bytes.Buffer
	keysListCmd.SetOut(&out)
	keysListCmd.SetErr(&out)
	err := keysListCmd.RunE(keysListCmd, []string{})
	require.NoError(t, err)

	s := out.String()
	assert.Contains(t, s, "openai")
	assert.Contains(t, s, "anthropic")
	assert.Contains(t, s, "sk-aaaa...aaaa")
	assert.Contains(t, s, "sk-ant-c...cccc")
	// masked by default: full plaintext key must not appear
	assert.NotContains(t, s, "sk-aaaaaaaaaaaaaaaaaaaa")
	assert.Contains(t, s, "3 key(s).")
}

func TestKeysList_FilterProvider(t *testing.T) {
	_ = seedKeysDB(t)
	flagKeysProvider = "openai"
	var out bytes.Buffer
	keysListCmd.SetOut(&out)
	keysListCmd.SetErr(&out)
	err := keysListCmd.RunE(keysListCmd, []string{})
	require.NoError(t, err)

	s := out.String()
	assert.Contains(t, s, "openai")
	assert.NotContains(t, s, "anthropic")
	assert.Contains(t, s, "2 key(s).")
}

func TestKeysShow_Hit(t *testing.T) {
	dbPath := seedKeysDB(t)
	id := firstFindingID(t, dbPath)

	var out bytes.Buffer
	keysShowCmd.SetOut(&out)
	keysShowCmd.SetErr(&out)
	err := keysShowCmd.RunE(keysShowCmd, []string{strconv.FormatInt(id, 10)})
	require.NoError(t, err)

	s := out.String()
	// show is always unmasked: full plaintext must be present
	assert.Contains(t, s, "sk-aaaaaaaaaaaaaaaaaaaa")
	assert.Contains(t, s, "Provider:")
	assert.Contains(t, s, "openai")
}

func TestKeysShow_Miss(t *testing.T) {
	_ = seedKeysDB(t)
	var out bytes.Buffer
	keysShowCmd.SetOut(&out)
	keysShowCmd.SetErr(&out)
	err := keysShowCmd.RunE(keysShowCmd, []string{"9999"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no finding with id 9999")
}

func TestKeysExport_JSON(t *testing.T) {
	_ = seedKeysDB(t)
	flagKeysFormat = "json"
	flagKeysOutFile = ""

	var out bytes.Buffer
	keysExportCmd.SetOut(&out)
	keysExportCmd.SetErr(&out)
	err := keysExportCmd.RunE(keysExportCmd, []string{})
	require.NoError(t, err)

	var parsed []map[string]interface{}
	require.NoError(t, json.Unmarshal(out.Bytes(), &parsed))
	assert.Len(t, parsed, 3)

	// Exports are unmasked — at least one row should carry the plaintext key.
	found := false
	for _, row := range parsed {
		if row["key"] == "sk-aaaaaaaaaaaaaaaaaaaa" {
			found = true
			break
		}
	}
	assert.True(t, found, "expected unmasked plaintext key in JSON export")
}

func TestKeysExport_CSVFile(t *testing.T) {
	_ = seedKeysDB(t)
	outFile := filepath.Join(t.TempDir(), "export.csv")
	flagKeysFormat = "csv"
	flagKeysOutFile = outFile

	var out bytes.Buffer
	keysExportCmd.SetOut(&out)
	keysExportCmd.SetErr(&out)
	err := keysExportCmd.RunE(keysExportCmd, []string{})
	require.NoError(t, err)

	data, err := os.ReadFile(outFile)
	require.NoError(t, err)

	r := csv.NewReader(strings.NewReader(string(data)))
	rows, err := r.ReadAll()
	require.NoError(t, err)

	require.Len(t, rows, 4) // header + 3 data
	assert.Equal(t, "id", rows[0][0])
	assert.Equal(t, "provider", rows[0][1])
}

func TestKeysDelete_WithYes(t *testing.T) {
	dbPath := seedKeysDB(t)
	id := firstFindingID(t, dbPath)
	flagKeysYes = true

	var out bytes.Buffer
	keysDeleteCmd.SetOut(&out)
	keysDeleteCmd.SetErr(&out)
	err := keysDeleteCmd.RunE(keysDeleteCmd, []string{strconv.FormatInt(id, 10)})
	require.NoError(t, err)
	assert.Contains(t, out.String(), "Deleted finding")

	// Verify list now returns 2.
	flagKeysYes = false
	var out2 bytes.Buffer
	keysListCmd.SetOut(&out2)
	keysListCmd.SetErr(&out2)
	require.NoError(t, keysListCmd.RunE(keysListCmd, []string{}))
	assert.Contains(t, out2.String(), "2 key(s).")
}

// NOTE: TestKeysCopy is omitted — the atotto/clipboard backend is unavailable
// in headless CI environments. The copy command path is exercised manually.
// NOTE: TestKeysVerify is omitted — verify requires live network calls to
// provider endpoints which is out of scope for unit tests.

// firstFindingID returns the smallest id present in the findings table of the
// seeded database. Used so tests do not depend on SQLite rowid sequencing.
func firstFindingID(t *testing.T, dbPath string) int64 {
	t.Helper()
	db, err := storage.Open(dbPath)
	require.NoError(t, err)
	defer db.Close()
	row := db.SQL().QueryRow(`SELECT MIN(id) FROM findings`)
	var id int64
	require.NoError(t, row.Scan(&id))
	return id
}
