package hostfile_test

import (
	"strings"
	"testing"

	"ekdns/hostfile"
)

// ---------------------------------------------------------------------------
// ParseManagedBlock tests
// ---------------------------------------------------------------------------

func TestParseManagedBlock_NoManagedBlock(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		"# some comment",
		"1.2.3.4 example.com",
	}
	prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if has {
		t.Fatal("expected HasManagedBlock=false")
	}
	if len(managed) != 0 {
		t.Errorf("expected empty managed, got %v", managed)
	}
	if len(postfix) != 0 {
		t.Errorf("expected empty postfix, got %v", postfix)
	}
	if len(prefix) != len(lines) {
		t.Errorf("expected all lines in prefix, got %v", prefix)
	}
}

func TestParseManagedBlock_ValidBlock(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.StartMarker,
		"1.1.1.1\thost1",
		"2.2.2.2\thost2",
		hostfile.EndMarker,
		"# trailing comment",
	}
	prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !has {
		t.Fatal("expected HasManagedBlock=true")
	}
	if len(prefix) != 1 || prefix[0] != "127.0.0.1 localhost" {
		t.Errorf("unexpected prefix: %v", prefix)
	}
	if len(managed) != 2 {
		t.Errorf("expected 2 managed lines, got %v", managed)
	}
	if len(postfix) != 1 || postfix[0] != "# trailing comment" {
		t.Errorf("unexpected postfix: %v", postfix)
	}
}

func TestParseManagedBlock_StartMarkerAtLine0(t *testing.T) {
	lines := []string{
		hostfile.StartMarker,
		"1.1.1.1\thost1",
		hostfile.EndMarker,
	}
	prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !has {
		t.Fatal("expected HasManagedBlock=true")
	}
	if len(prefix) != 0 {
		t.Errorf("expected empty prefix when start marker is at line 0, got %v", prefix)
	}
	if len(managed) != 1 || managed[0] != "1.1.1.1\thost1" {
		t.Errorf("unexpected managed: %v", managed)
	}
	if len(postfix) != 0 {
		t.Errorf("expected empty postfix, got %v", postfix)
	}
}

func TestParseManagedBlock_CorruptStartWithoutEnd(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.StartMarker,
		"1.1.1.1\thost1",
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for start marker without end marker")
	}
	if !strings.Contains(err.Error(), "start marker found") {
		t.Errorf("error should mention 'start marker found', got: %v", err)
	}
	if !strings.Contains(err.Error(), "no end marker") {
		t.Errorf("error should mention 'no end marker', got: %v", err)
	}
}

func TestParseManagedBlock_CorruptEndWithoutStart(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		"1.1.1.1\thost1",
		hostfile.EndMarker,
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for end marker without start marker")
	}
	if !strings.Contains(err.Error(), "end marker found") {
		t.Errorf("error should mention 'end marker found', got: %v", err)
	}
	if !strings.Contains(err.Error(), "no start marker") {
		t.Errorf("error should mention 'no start marker', got: %v", err)
	}
}

func TestParseManagedBlock_CorruptEndBeforeStart(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.EndMarker,
		"1.1.1.1\thost1",
		hostfile.StartMarker,
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for end marker before start marker")
	}
	if !strings.Contains(err.Error(), "end marker") {
		t.Errorf("error should mention 'end marker', got: %v", err)
	}
	if !strings.Contains(err.Error(), "before start marker") {
		t.Errorf("error should mention 'before start marker', got: %v", err)
	}
}

func TestParseManagedBlock_CorruptDuplicateMarkers(t *testing.T) {
	lines := []string{
		hostfile.StartMarker,
		"1.1.1.1\thost1",
		hostfile.EndMarker,
		hostfile.StartMarker, // duplicate
		"2.2.2.2\thost2",
		hostfile.EndMarker,
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for duplicate markers")
	}
	if !strings.Contains(err.Error(), "duplicate") {
		t.Errorf("error should mention 'duplicate', got: %v", err)
	}
}

// ---------------------------------------------------------------------------
// ParseManagedBlock — legacy marker tests
// ---------------------------------------------------------------------------

func TestParseManagedBlock_LegacyMarkers(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.LegacyStartMarker,
		"1.1.1.1\thost1",
		"2.2.2.2\thost2",
		hostfile.LegacyEndMarker,
		"# trailing comment",
	}
	prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !has {
		t.Fatal("expected HasManagedBlock=true for legacy markers")
	}
	if len(prefix) != 1 || prefix[0] != "127.0.0.1 localhost" {
		t.Errorf("unexpected prefix: %v", prefix)
	}
	if len(managed) != 2 {
		t.Errorf("expected 2 managed lines, got %v", managed)
	}
	if len(postfix) != 1 || postfix[0] != "# trailing comment" {
		t.Errorf("unexpected postfix: %v", postfix)
	}
}

func TestParseManagedBlock_LegacyStartMarkerAtLine0(t *testing.T) {
	lines := []string{
		hostfile.LegacyStartMarker,
		"1.1.1.1\thost1",
		hostfile.LegacyEndMarker,
	}
	prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !has {
		t.Fatal("expected HasManagedBlock=true")
	}
	if len(prefix) != 0 {
		t.Errorf("expected empty prefix, got %v", prefix)
	}
	if len(managed) != 1 || managed[0] != "1.1.1.1\thost1" {
		t.Errorf("unexpected managed: %v", managed)
	}
	if len(postfix) != 0 {
		t.Errorf("expected empty postfix, got %v", postfix)
	}
}

func TestParseManagedBlock_LegacyCorruptStartWithoutEnd(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.LegacyStartMarker,
		"1.1.1.1\thost1",
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for legacy start marker without end marker")
	}
}

func TestParseManagedBlock_LegacyCorruptEndWithoutStart(t *testing.T) {
	lines := []string{
		"127.0.0.1 localhost",
		"1.1.1.1\thost1",
		hostfile.LegacyEndMarker,
	}
	_, _, _, _, err := hostfile.ParseManagedBlock(lines)
	if err == nil {
		t.Fatal("expected error for legacy end marker without start marker")
	}
}

func TestParseManagedBlock_MixedOldStartNewEnd(t *testing.T) {
	// Legacy start with current end — still a valid block (both substrings match).
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.LegacyStartMarker,
		"1.1.1.1\thost1",
		hostfile.EndMarker,
	}
	_, managed, _, has, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !has {
		t.Fatal("expected HasManagedBlock=true for mixed markers")
	}
	if len(managed) != 1 {
		t.Errorf("expected 1 managed line, got %v", managed)
	}
}

func TestParseManagedBlock_LegacyUpgradedOnWrite(t *testing.T) {
	// Verify that a legacy block is parsed and then reassembled with current markers.
	lines := []string{
		"127.0.0.1 localhost",
		hostfile.LegacyStartMarker,
		"1.1.1.1\thost1",
		hostfile.LegacyEndMarker,
		"# trailing",
	}
	prefix, managed, postfix, _, err := hostfile.ParseManagedBlock(lines)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	result := hostfile.AssembleContent(prefix, managed, postfix)
	// The reassembled output should use the current markers, not legacy.
	found := false
	for _, l := range result {
		if l == hostfile.StartMarker {
			found = true
		}
		if l == hostfile.LegacyStartMarker {
			t.Fatal("reassembled content should not contain legacy start marker")
		}
	}
	if !found {
		t.Fatal("reassembled content should contain current start marker")
	}
}

// ---------------------------------------------------------------------------
// AddEntries tests
// ---------------------------------------------------------------------------

func TestAddEntries_AddsNewEntries(t *testing.T) {
	existing := []string{"1.1.1.1\thost1"}
	entries := []hostfile.DNSEntry{{IP: "2.2.2.2", Hostname: "host2"}}
	result := hostfile.AddEntries(existing, entries)
	if len(result) != 2 {
		t.Fatalf("expected 2 entries, got %d: %v", len(result), result)
	}
	if result[0] != "1.1.1.1\thost1" {
		t.Errorf("unexpected entry[0]: %q", result[0])
	}
	if result[1] != "2.2.2.2\thost2" {
		t.Errorf("unexpected entry[1]: %q", result[1])
	}
}

func TestAddEntries_ReplacesExistingHostname(t *testing.T) {
	existing := []string{"1.1.1.1\thost1", "2.2.2.2\thost2"}
	// Replace host1 with a new IP
	entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "host1"}}
	result := hostfile.AddEntries(existing, entries)
	// host1 should be replaced, host2 preserved
	if len(result) != 2 {
		t.Fatalf("expected 2 entries, got %d: %v", len(result), result)
	}
	found := false
	for _, line := range result {
		if strings.Contains(line, "1.1.1.1") && strings.Contains(line, "host1") {
			t.Errorf("old entry for host1 should have been replaced: %v", result)
		}
		if strings.Contains(line, "9.9.9.9") && strings.Contains(line, "host1") {
			found = true
		}
	}
	if !found {
		t.Errorf("new entry for host1 not found: %v", result)
	}
}

func TestAddEntries_DoesNotClobberSubdomains(t *testing.T) {
	// Regression: substring matching would cause adding "example.com" to
	// incorrectly remove "sub.example.com" entries.
	existing := []string{
		"1.1.1.1\tsub.example.com",
		"2.2.2.2\tother.example.com",
	}
	entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "example.com"}}
	result := hostfile.AddEntries(existing, entries)
	if len(result) != 3 {
		t.Fatalf("expected 3 entries (2 existing + 1 new), got %d: %v", len(result), result)
	}
	for _, line := range result {
		if line == "1.1.1.1\tsub.example.com" || line == "2.2.2.2\tother.example.com" || line == "9.9.9.9\texample.com" {
			continue
		}
		t.Errorf("unexpected line: %q", line)
	}
}

func TestAddEntries_MatchesAliasColumn(t *testing.T) {
	// A hosts-file line can have aliases: "IP primary alias1 alias2"
	// Replacing "alias1" should remove the entire line.
	existing := []string{"1.1.1.1\tprimary\talias1"}
	entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "alias1"}}
	result := hostfile.AddEntries(existing, entries)
	for _, line := range result {
		if strings.Contains(line, "primary") {
			t.Errorf("line with alias1 should have been removed, but found: %q", line)
		}
	}
}

func TestAddEntries_CaseInsensitive(t *testing.T) {
	// RFC 4343: hostnames are case-insensitive.
	existing := []string{"1.1.1.1\tHost1.Example.COM"}
	entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "host1.example.com"}}
	result := hostfile.AddEntries(existing, entries)
	if len(result) != 1 {
		t.Fatalf("expected 1 entry (old replaced), got %d: %v", len(result), result)
	}
	if result[0] != "9.9.9.9\thost1.example.com" {
		t.Errorf("expected new entry, got %q", result[0])
	}
}

func TestAddEntries_Deduplicates(t *testing.T) {
	existing := []string{"1.1.1.1\thost1"}
	entries := []hostfile.DNSEntry{
		{IP: "2.2.2.2", Hostname: "host2"},
		{IP: "2.2.2.2", Hostname: "host2"}, // duplicate
	}
	result := hostfile.AddEntries(existing, entries)
	count := 0
	for _, line := range result {
		if line == "2.2.2.2\thost2" {
			count++
		}
	}
	if count != 1 {
		t.Errorf("expected exactly 1 host2 entry after dedup, got %d: %v", count, result)
	}
}

// ---------------------------------------------------------------------------
// AssembleContent tests
// ---------------------------------------------------------------------------

func TestAssembleContent_WithManagedBlock(t *testing.T) {
	prefix := []string{"127.0.0.1 localhost"}
	managed := []string{"1.1.1.1\thost1"}
	postfix := []string{"# end"}
	result := hostfile.AssembleContent(prefix, managed, postfix)
	expected := []string{
		"127.0.0.1 localhost",
		hostfile.StartMarker,
		"1.1.1.1\thost1",
		hostfile.EndMarker,
		"# end",
	}
	if len(result) != len(expected) {
		t.Fatalf("expected %d lines, got %d: %v", len(expected), len(result), result)
	}
	for i, line := range expected {
		if result[i] != line {
			t.Errorf("line %d: expected %q, got %q", i, line, result[i])
		}
	}
}

func TestAssembleContent_EmptyManagedOmitsBlock(t *testing.T) {
	prefix := []string{"127.0.0.1 localhost"}
	postfix := []string{"# end"}
	result := hostfile.AssembleContent(prefix, nil, postfix)
	if len(result) != 2 {
		t.Fatalf("expected 2 lines (no managed block), got %d: %v", len(result), result)
	}
	for _, line := range result {
		if strings.Contains(line, "ekDNSHelper") {
			t.Errorf("managed block markers should be absent when managed is empty: %v", result)
		}
	}
}

// ---------------------------------------------------------------------------
// RemoveByHostname tests (T018)
// ---------------------------------------------------------------------------

func TestRemoveByHostname_SingleHost(t *testing.T) {
	existing := []string{
		"1.1.1.1\thost1",
		"2.2.2.2\thost2",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host1"})
	if len(result) != 1 {
		t.Fatalf("expected 1 entry, got %d: %v", len(result), result)
	}
	if result[0] != "2.2.2.2\thost2" {
		t.Errorf("expected host2 entry to remain, got %q", result[0])
	}
}

func TestRemoveByHostname_MultipleHosts(t *testing.T) {
	existing := []string{
		"1.1.1.1\thost1",
		"2.2.2.2\thost2",
		"3.3.3.3\thost3",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host1", "host2"})
	if len(result) != 1 {
		t.Fatalf("expected 1 entry, got %d: %v", len(result), result)
	}
	if result[0] != "3.3.3.3\thost3" {
		t.Errorf("expected host3 entry to remain, got %q", result[0])
	}
}

func TestRemoveByHostname_HostNotFound(t *testing.T) {
	existing := []string{
		"1.1.1.1\thost1",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host2"})
	if len(result) != 1 {
		t.Fatalf("expected content unchanged (1 entry), got %d: %v", len(result), result)
	}
	if result[0] != "1.1.1.1\thost1" {
		t.Errorf("expected host1 entry unchanged, got %q", result[0])
	}
}

func TestRemoveByHostname_AllHostsRemoved(t *testing.T) {
	existing := []string{
		"1.1.1.1\thost1",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host1"})
	if len(result) != 0 {
		t.Errorf("expected empty slice, got %d entries: %v", len(result), result)
	}
}

func TestRemoveByHostname_RegressionNoDuplicates(t *testing.T) {
	// This is the R-004 regression test. The legacy code's loop-order bug
	// produced duplicates when removing multiple hosts at once.
	existing := []string{
		"1.1.1.1\thost1",
		"2.2.2.2\thost2",
		"3.3.3.3\thost3",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host1", "host2"})
	if len(result) != 1 {
		t.Fatalf("expected exactly 1 entry after multi-host removal, got %d: %v", len(result), result)
	}
	if result[0] != "3.3.3.3\thost3" {
		t.Errorf("expected only host3 to remain, got %q", result[0])
	}
}

func TestRemoveByHostname_DoesNotClobberSubdomains(t *testing.T) {
	// Regression: substring matching would remove "sub.example.com" when
	// only "example.com" was requested for removal.
	existing := []string{
		"1.1.1.1\texample.com",
		"2.2.2.2\tsub.example.com",
		"3.3.3.3\tmy-example.com",
	}
	result := hostfile.RemoveByHostname(existing, []string{"example.com"})
	if len(result) != 2 {
		t.Fatalf("expected 2 remaining entries, got %d: %v", len(result), result)
	}
	if result[0] != "2.2.2.2\tsub.example.com" {
		t.Errorf("expected sub.example.com to remain, got %q", result[0])
	}
	if result[1] != "3.3.3.3\tmy-example.com" {
		t.Errorf("expected my-example.com to remain, got %q", result[1])
	}
}

func TestRemoveByHostname_MatchesAliasColumn(t *testing.T) {
	existing := []string{
		"1.1.1.1\tprimary\talias1",
		"2.2.2.2\thost2",
	}
	result := hostfile.RemoveByHostname(existing, []string{"alias1"})
	if len(result) != 1 {
		t.Fatalf("expected 1 remaining entry, got %d: %v", len(result), result)
	}
	if result[0] != "2.2.2.2\thost2" {
		t.Errorf("expected host2 to remain, got %q", result[0])
	}
}

func TestRemoveByHostname_CaseInsensitive(t *testing.T) {
	existing := []string{
		"1.1.1.1\tHost1.Example.COM",
		"2.2.2.2\thost2",
	}
	result := hostfile.RemoveByHostname(existing, []string{"host1.example.com"})
	if len(result) != 1 {
		t.Fatalf("expected 1 remaining entry, got %d: %v", len(result), result)
	}
	if result[0] != "2.2.2.2\thost2" {
		t.Errorf("expected host2 to remain, got %q", result[0])
	}
}

// ---------------------------------------------------------------------------
// RemoveAll tests (T018)
// ---------------------------------------------------------------------------

func TestRemoveAll_ReturnEmptySlice(t *testing.T) {
	result := hostfile.RemoveAll()
	if len(result) != 0 {
		t.Errorf("expected empty result, got %d entries: %v", len(result), result)
	}
}

func TestRemoveAll_AssembleContentOmitsBlock(t *testing.T) {
	// When RemoveAll result is passed to AssembleContent, no managed block markers
	// should appear in the assembled output.
	prefix := []string{"127.0.0.1 localhost"}
	postfix := []string{"# trailing"}
	result := hostfile.AssembleContent(prefix, hostfile.RemoveAll(), postfix)
	if len(result) != 2 {
		t.Fatalf("expected 2 lines with no managed block, got %d: %v", len(result), result)
	}
	for _, line := range result {
		if strings.Contains(line, "ekDNSHelper") {
			t.Errorf("managed block markers should be absent after RemoveAll: %v", result)
		}
	}
}
