package services

import (
	"path/filepath"
	"strings"
	"testing"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	syncsvc "git.mchus.pro/mchus/quoteforge/internal/services/sync"
)

func TestParseCFXMLWorkspaceGroupsSoftwareIntoConfiguration(t *testing.T) {
	const sample = `<?xml version="1.0" encoding="UTF-8"?>
<CFXML>
  <thisDocumentIdentifier>
    <ProprietaryDocumentIdentifier>CFXML.workspace-test</ProprietaryDocumentIdentifier>
  </thisDocumentIdentifier>
  <CFData>
    <ProprietaryInformation>
      <Name>Currencies</Name>
      <Value>USD</Value>
    </ProprietaryInformation>
    <ProductLineItem>
      <ProductLineNumber>1000</ProductLineNumber>
      <ItemNo>1000</ItemNo>
      <TransactionType>NEW</TransactionType>
      <ProprietaryGroupIdentifier>1000</ProprietaryGroupIdentifier>
      <ConfigurationGroupLineNumberReference>100</ConfigurationGroupLineNumberReference>
      <Quantity>6</Quantity>
      <ProductIdentification>
        <PartnerProductIdentification>
          <ProprietaryProductIdentifier>7DG9-CTO1WW</ProprietaryProductIdentifier>
          <ProductDescription>ThinkSystem SR630 V4</ProductDescription>
          <ProductName>#1</ProductName>
          <ProductTypeCode>Hardware</ProductTypeCode>
        </PartnerProductIdentification>
      </ProductIdentification>
      <UnitListPrice>
        <FinancialAmount>
          <GlobalCurrencyCode>USD</GlobalCurrencyCode>
          <MonetaryAmount>100.00</MonetaryAmount>
        </FinancialAmount>
      </UnitListPrice>
      <ProductSubLineItem>
        <LineNumber>1001</LineNumber>
        <TransactionType>ADD</TransactionType>
        <Quantity>2</Quantity>
        <ProductIdentification>
          <PartnerProductIdentification>
            <ProprietaryProductIdentifier>CPU-1</ProprietaryProductIdentifier>
            <ProductDescription>CPU</ProductDescription>
            <ProductCharacter>PROCESSOR</ProductCharacter>
          </PartnerProductIdentification>
        </ProductIdentification>
        <UnitListPrice>
          <FinancialAmount>
            <GlobalCurrencyCode>USD</GlobalCurrencyCode>
            <MonetaryAmount>0</MonetaryAmount>
          </FinancialAmount>
        </UnitListPrice>
      </ProductSubLineItem>
    </ProductLineItem>
    <ProductLineItem>
      <ProductLineNumber>2000</ProductLineNumber>
      <ItemNo>2000</ItemNo>
      <TransactionType>NEW</TransactionType>
      <ProprietaryGroupIdentifier>1000</ProprietaryGroupIdentifier>
      <ConfigurationGroupLineNumberReference>100</ConfigurationGroupLineNumberReference>
      <Quantity>6</Quantity>
      <ProductIdentification>
        <PartnerProductIdentification>
          <ProprietaryProductIdentifier>7S0X-CTO8WW</ProprietaryProductIdentifier>
          <ProductDescription>XClarity Controller Prem-FOD</ProductDescription>
          <ProductName>software1</ProductName>
          <ProductTypeCode>Software</ProductTypeCode>
        </PartnerProductIdentification>
      </ProductIdentification>
      <UnitListPrice>
        <FinancialAmount>
          <GlobalCurrencyCode>USD</GlobalCurrencyCode>
          <MonetaryAmount>25.00</MonetaryAmount>
        </FinancialAmount>
      </UnitListPrice>
      <ProductSubLineItem>
        <LineNumber>2001</LineNumber>
        <TransactionType>ADD</TransactionType>
        <Quantity>1</Quantity>
        <ProductIdentification>
          <PartnerProductIdentification>
            <ProprietaryProductIdentifier>LIC-1</ProprietaryProductIdentifier>
            <ProductDescription>License</ProductDescription>
            <ProductCharacter>SOFTWARE</ProductCharacter>
          </PartnerProductIdentification>
        </ProductIdentification>
        <UnitListPrice>
          <FinancialAmount>
            <GlobalCurrencyCode>USD</GlobalCurrencyCode>
            <MonetaryAmount>0</MonetaryAmount>
          </FinancialAmount>
        </UnitListPrice>
      </ProductSubLineItem>
    </ProductLineItem>
    <ProductLineItem>
      <ProductLineNumber>3000</ProductLineNumber>
      <ItemNo>3000</ItemNo>
      <TransactionType>NEW</TransactionType>
      <ProprietaryGroupIdentifier>3000</ProprietaryGroupIdentifier>
      <ConfigurationGroupLineNumberReference>100</ConfigurationGroupLineNumberReference>
      <Quantity>2</Quantity>
      <ProductIdentification>
        <PartnerProductIdentification>
          <ProprietaryProductIdentifier>7DG9-CTO1WW</ProprietaryProductIdentifier>
          <ProductDescription>ThinkSystem SR630 V4</ProductDescription>
          <ProductName>#2</ProductName>
          <ProductTypeCode>Hardware</ProductTypeCode>
        </PartnerProductIdentification>
      </ProductIdentification>
      <UnitListPrice>
        <FinancialAmount>
          <GlobalCurrencyCode>USD</GlobalCurrencyCode>
          <MonetaryAmount>90.00</MonetaryAmount>
        </FinancialAmount>
      </UnitListPrice>
    </ProductLineItem>
  </CFData>
</CFXML>`

	workspace, err := parseCFXMLWorkspace([]byte(sample), "sample.xml")
	if err != nil {
		t.Fatalf("parseCFXMLWorkspace: %v", err)
	}

	if workspace.SourceFormat != "CFXML" {
		t.Fatalf("unexpected source format: %q", workspace.SourceFormat)
	}
	if len(workspace.Configurations) != 2 {
		t.Fatalf("expected 2 configurations, got %d", len(workspace.Configurations))
	}

	first := workspace.Configurations[0]
	if first.GroupID != "1000" {
		t.Fatalf("expected first group 1000, got %q", first.GroupID)
	}
	if first.Name != "#1" {
		t.Fatalf("expected first config name #1, got %q", first.Name)
	}
	if first.ServerCount != 6 {
		t.Fatalf("expected first server count 6, got %d", first.ServerCount)
	}
	if len(first.Rows) != 4 {
		t.Fatalf("expected 4 vendor rows in first config, got %d", len(first.Rows))
	}

	foundSoftwareTopLevel := false
	foundSoftwareSubRow := false
	foundPrimaryTopLevelQty := 0
	for _, row := range first.Rows {
		if row.VendorPartnumber == "7DG9-CTO1WW" {
			foundPrimaryTopLevelQty = row.Quantity
		}
		if row.VendorPartnumber == "7S0X-CTO8WW" {
			foundSoftwareTopLevel = true
		}
		if row.VendorPartnumber == "LIC-1" {
			foundSoftwareSubRow = true
		}
	}
	if !foundSoftwareTopLevel {
		t.Fatalf("expected software top-level row to stay inside configuration")
	}
	if !foundSoftwareSubRow {
		t.Fatalf("expected software sub-row to stay inside configuration")
	}
	if foundPrimaryTopLevelQty != 1 {
		t.Fatalf("expected primary top-level qty normalized to 1, got %d", foundPrimaryTopLevelQty)
	}
	if first.TotalPrice == nil || *first.TotalPrice != 750 {
		t.Fatalf("expected first total price 750, got %+v", first.TotalPrice)
	}

	second := workspace.Configurations[1]
	if second.Name != "#2" {
		t.Fatalf("expected second config name #2, got %q", second.Name)
	}
	if len(second.Rows) != 1 {
		t.Fatalf("expected second config to contain single top-level row, got %d", len(second.Rows))
	}
}

func TestImportVendorWorkspaceToProject_AutoResolvesAndAppliesEstimate(t *testing.T) {
	local, err := localdb.New(filepath.Join(t.TempDir(), "local.db"))
	if err != nil {
		t.Fatalf("init local db: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	projectName := "OPS-2079"
	if err := local.SaveProject(&localdb.LocalProject{
		UUID:          "project-1",
		OwnerUsername: "tester",
		Code:          "OPS-2079",
		Variant:       "",
		Name:          &projectName,
		IsActive:      true,
		CreatedAt:     time.Now(),
		UpdatedAt:     time.Now(),
	}); err != nil {
		t.Fatalf("save project: %v", err)
	}

	if err := local.SaveLocalPricelist(&localdb.LocalPricelist{
		ServerID:  101,
		Source:    "estimate",
		Version:   "E-1",
		Name:      "Estimate",
		CreatedAt: time.Now(),
		SyncedAt:  time.Now(),
	}); err != nil {
		t.Fatalf("save estimate pricelist: %v", err)
	}
	estimatePL, err := local.GetLocalPricelistByServerID(101)
	if err != nil {
		t.Fatalf("get estimate pricelist: %v", err)
	}
	if err := local.SaveLocalPricelistItems([]localdb.LocalPricelistItem{
		{PricelistID: estimatePL.ID, LotName: "CPU_INTEL_6747P", Price: 1000},
		{PricelistID: estimatePL.ID, LotName: "LICENSE_XCC", Price: 50},
	}); err != nil {
		t.Fatalf("save estimate items: %v", err)
	}

	bookRepo := local.DB()
	if err := bookRepo.Create(&localdb.LocalPartnumberBook{
		ServerID:        501,
		Version:         "B-1",
		CreatedAt:       time.Now(),
		IsActive:        true,
		PartnumbersJSON: localdb.LocalStringList{"CPU-1", "LIC-1"},
	}).Error; err != nil {
		t.Fatalf("save active book: %v", err)
	}
	if err := bookRepo.Create([]localdb.LocalPartnumberBookItem{
		{Partnumber: "CPU-1", LotsJSON: localdb.LocalPartnumberBookLots{{LotName: "CPU_INTEL_6747P", Qty: 1}}},
		{Partnumber: "LIC-1", LotsJSON: localdb.LocalPartnumberBookLots{{LotName: "LICENSE_XCC", Qty: 1}}},
	}).Error; err != nil {
		t.Fatalf("save book items: %v", err)
	}

	service := NewLocalConfigurationService(local, syncsvc.NewService(nil, local), &QuoteService{}, func() bool { return false })

	const sample = `<?xml version="1.0" encoding="UTF-8"?>
<CFXML>
  <thisDocumentIdentifier>
    <ProprietaryDocumentIdentifier>CFXML.workspace-test</ProprietaryDocumentIdentifier>
  </thisDocumentIdentifier>
  <CFData>
    <ProductLineItem>
      <ProductLineNumber>1000</ProductLineNumber>
      <ItemNo>1000</ItemNo>
      <TransactionType>NEW</TransactionType>
      <ProprietaryGroupIdentifier>1000</ProprietaryGroupIdentifier>
      <Quantity>2</Quantity>
      <ProductIdentification>
        <PartnerProductIdentification>
          <ProprietaryProductIdentifier>7DG9-CTO1WW</ProprietaryProductIdentifier>
          <ProductDescription>ThinkSystem SR630 V4</ProductDescription>
          <ProductName>#1</ProductName>
          <ProductTypeCode>Hardware</ProductTypeCode>
        </PartnerProductIdentification>
      </ProductIdentification>
      <ProductSubLineItem>
        <LineNumber>1001</LineNumber>
        <Quantity>2</Quantity>
        <ProductIdentification>
          <PartnerProductIdentification>
            <ProprietaryProductIdentifier>CPU-1</ProprietaryProductIdentifier>
            <ProductDescription>CPU</ProductDescription>
          </PartnerProductIdentification>
        </ProductIdentification>
      </ProductSubLineItem>
    </ProductLineItem>
    <ProductLineItem>
      <ProductLineNumber>2000</ProductLineNumber>
      <ItemNo>2000</ItemNo>
      <TransactionType>NEW</TransactionType>
      <ProprietaryGroupIdentifier>1000</ProprietaryGroupIdentifier>
      <Quantity>2</Quantity>
      <ProductIdentification>
        <PartnerProductIdentification>
          <ProprietaryProductIdentifier>7S0X-CTO8WW</ProprietaryProductIdentifier>
          <ProductDescription>XClarity Controller</ProductDescription>
          <ProductName>software1</ProductName>
          <ProductTypeCode>Software</ProductTypeCode>
        </PartnerProductIdentification>
      </ProductIdentification>
      <ProductSubLineItem>
        <LineNumber>2001</LineNumber>
        <Quantity>1</Quantity>
        <ProductIdentification>
          <PartnerProductIdentification>
            <ProprietaryProductIdentifier>LIC-1</ProprietaryProductIdentifier>
            <ProductDescription>License</ProductDescription>
          </PartnerProductIdentification>
        </ProductIdentification>
      </ProductSubLineItem>
    </ProductLineItem>
  </CFData>
</CFXML>`

	result, err := service.ImportVendorWorkspaceToProject("project-1", "sample.xml", []byte(sample), "tester")
	if err != nil {
		t.Fatalf("ImportVendorWorkspaceToProject: %v", err)
	}
	if result.Imported != 1 || len(result.Configs) != 1 {
		t.Fatalf("unexpected import result: %+v", result)
	}

	cfg, err := local.GetConfigurationByUUID(result.Configs[0].UUID)
	if err != nil {
		t.Fatalf("load imported config: %v", err)
	}
	if cfg.PricelistID == nil || *cfg.PricelistID != 101 {
		t.Fatalf("expected estimate pricelist id 101, got %+v", cfg.PricelistID)
	}
	if len(cfg.VendorSpec) != 4 {
		t.Fatalf("expected 4 vendor spec rows, got %d", len(cfg.VendorSpec))
	}
	if len(cfg.Items) != 2 {
		t.Fatalf("expected 2 cart items, got %d", len(cfg.Items))
	}
	if cfg.Items[0].LotName != "CPU_INTEL_6747P" || cfg.Items[0].Quantity != 2 || cfg.Items[0].UnitPrice != 1000 {
		t.Fatalf("unexpected first item: %+v", cfg.Items[0])
	}
	if cfg.Items[1].LotName != "LICENSE_XCC" || cfg.Items[1].Quantity != 1 || cfg.Items[1].UnitPrice != 50 {
		t.Fatalf("unexpected second item: %+v", cfg.Items[1])
	}
	if cfg.TotalPrice == nil || *cfg.TotalPrice != 4100 {
		t.Fatalf("expected total price 4100 for 2 servers, got %+v", cfg.TotalPrice)
	}

	foundCPU := false
	foundLIC := false
	for _, row := range cfg.VendorSpec {
		if row.VendorPartnumber == "CPU-1" {
			foundCPU = true
			if len(row.LotMappings) != 1 || row.LotMappings[0].LotName != "CPU_INTEL_6747P" || row.LotMappings[0].QuantityPerPN != 1 {
				t.Fatalf("unexpected CPU mappings: %+v", row.LotMappings)
			}
		}
		if row.VendorPartnumber == "LIC-1" {
			foundLIC = true
			if len(row.LotMappings) != 1 || row.LotMappings[0].LotName != "LICENSE_XCC" || row.LotMappings[0].QuantityPerPN != 1 {
				t.Fatalf("unexpected LIC mappings: %+v", row.LotMappings)
			}
		}
	}
	if !foundCPU || !foundLIC {
		t.Fatalf("expected resolved rows for CPU and LIC in vendor spec")
	}
}

func TestParseInspurBOM(t *testing.T) {
	const sample = `|CPU_AMD_9535-EPYC2.4_64C_256M_300W*1
|Mem_64G_DDR5-6400MHz_ECC-RDIMM*1
|2.5 NVMe Bays*4
|2.5 or 3.5 SATA Bays*8
|FrontHDModuleBackPlane_4SAS or 4U.2_3.5x4_GEN5*1
|FrontHDModuleBackPlane_4SAS or 4U.2_3.5x4_GEN5*2
|RAID_IAG_2RO_9230_N_M.2_PCIE2_HS *1
|SSD_SA_480M2TD_MZNL3480HCLR_T2_6_PM893*2
|NIC_100Gbps_2Port_LC_Nvidia_CX6DX_PCIe_GEN4*1
|Riser_X16+X8+X8_G5-J4J6-A*1
|PowerSupply_1300W_Titanium_220VACor240VDC_GaN*2
|PowerCord_1.5m_C14_C13_CN+CNHK+CNTW+US+UK+EU+AU+SG+ZA+RU+KR*2
|Rail_Slider-Drop-in_760mm_2U-EN*1
|PKACCY_470x285x63_Box-Blankspace_General*1
|Chassis_3.5x12_6PCIE*1
|MB_AMD_Non*1
|Fan_23000rpm_6056*6
|Software-KSManage*1
|TPM_2.0_NON-MainLand_SPI-INF*1
|【CA&SA】KR2180E3-A0 3 years RTV HK Service*1
|【CA&SA】KR2180E3-A0 3 years Data Media Retention Service*1`

	workspace, err := parseInspurBOM([]byte(sample), "KR2180E3-A0.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if workspace.SourceFormat != "Inspur" {
		t.Fatalf("expected SourceFormat Inspur, got %q", workspace.SourceFormat)
	}
	if len(workspace.Configurations) != 1 {
		t.Fatalf("expected 1 configuration, got %d", len(workspace.Configurations))
	}
	cfg := workspace.Configurations[0]
	if cfg.Name != "KR2180E3-A0" {
		t.Fatalf("expected name KR2180E3-A0, got %q", cfg.Name)
	}
	if cfg.ServerCount != 1 {
		t.Fatalf("expected ServerCount 1, got %d", cfg.ServerCount)
	}
	const wantRows = 21
	if len(cfg.Rows) != wantRows {
		t.Fatalf("expected %d rows, got %d", wantRows, len(cfg.Rows))
	}

	rowsByPN := make(map[string]localdb.VendorSpecItem, len(cfg.Rows))
	for _, r := range cfg.Rows {
		rowsByPN[r.VendorPartnumber] = r
	}

	cpu, ok := rowsByPN["CPU_AMD_9535-EPYC2.4_64C_256M_300W"]
	if !ok {
		t.Fatal("expected CPU row not found")
	}
	if cpu.Quantity != 1 {
		t.Fatalf("CPU: expected qty 1, got %d", cpu.Quantity)
	}

	psu, ok := rowsByPN["PowerSupply_1300W_Titanium_220VACor240VDC_GaN"]
	if !ok {
		t.Fatal("expected PSU row not found")
	}
	if psu.Quantity != 2 {
		t.Fatalf("PSU: expected qty 2, got %d", psu.Quantity)
	}

	fan, ok := rowsByPN["Fan_23000rpm_6056"]
	if !ok {
		t.Fatal("expected Fan row not found")
	}
	if fan.Quantity != 6 {
		t.Fatalf("Fan: expected qty 6, got %d", fan.Quantity)
	}

	// RAID partnumber has trailing space before *, must be trimmed
	raid, ok := rowsByPN["RAID_IAG_2RO_9230_N_M.2_PCIE2_HS"]
	if !ok {
		t.Fatal("expected RAID row not found (check whitespace trimming)")
	}
	if raid.Quantity != 1 {
		t.Fatalf("RAID: expected qty 1, got %d", raid.Quantity)
	}
}

func TestParseInspurBOMWithoutPipe(t *testing.T) {
	const sample = `CPU_AMD_9535*2
Mem_64G_DDR5*4
PowerSupply_1300W*2`

	workspace, err := parseInspurBOM([]byte(sample), "config.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(workspace.Configurations[0].Rows) != 3 {
		t.Fatalf("expected 3 rows, got %d", len(workspace.Configurations[0].Rows))
	}
	if workspace.Configurations[0].Rows[0].VendorPartnumber != "CPU_AMD_9535" {
		t.Fatalf("unexpected pn: %q", workspace.Configurations[0].Rows[0].VendorPartnumber)
	}
	if workspace.Configurations[0].Rows[0].Quantity != 2 {
		t.Fatalf("unexpected qty: %d", workspace.Configurations[0].Rows[0].Quantity)
	}
}

func TestParseQuoteForgeCSV(t *testing.T) {
	// Format mirrors ToCSV output: col[0]=Line, col[1]=Type, col[2]=p/n,
	// col[3]=Description, col[4]=Qty(1pcs), col[5]=Qty(total), col[6]=Price(1pcs), col[7]=Price(total)
	const sample = "\xEF\xBB\xBF" + // UTF-8 BOM
		"Line;Type;p/n;Description;Qty (1 pcs.);Qty (total);Price (1 pcs.);Price (total)\n" +
		"10;;DL380-ARTICLE;;;2;10470;20 940\n" +
		";MEMORY;MB_INTEL_A1;;1;;2074,5;\n" +
		";CPU;CPU_XEON_X;;2;;5100;\n" +
		"\n" +
		"20;;DL380-ARTICLE-2;;;1;8000;8 000\n" +
		";STORAGE;SSD_NVMe;;4;;1200;\n"

	workspace, err := parseQuoteForgeCSV([]byte(sample), "project.csv")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if workspace.SourceFormat != "QuoteForgeCSV" {
		t.Fatalf("expected SourceFormat QuoteForgeCSV, got %q", workspace.SourceFormat)
	}
	if len(workspace.Configurations) != 2 {
		t.Fatalf("expected 2 configurations, got %d", len(workspace.Configurations))
	}

	cfg1 := workspace.Configurations[0]
	if cfg1.Article != "DL380-ARTICLE" {
		t.Fatalf("cfg1 article: want DL380-ARTICLE, got %q", cfg1.Article)
	}
	if cfg1.ServerCount != 2 {
		t.Fatalf("cfg1 server_count: want 2, got %d", cfg1.ServerCount)
	}
	if len(cfg1.DirectItems) != 2 {
		t.Fatalf("cfg1 items: want 2, got %d", len(cfg1.DirectItems))
	}
	if cfg1.DirectItems[0].LotName != "MB_INTEL_A1" || cfg1.DirectItems[0].Quantity != 1 {
		t.Fatalf("cfg1 item[0]: %+v", cfg1.DirectItems[0])
	}
	if cfg1.DirectItems[1].LotName != "CPU_XEON_X" || cfg1.DirectItems[1].Quantity != 2 {
		t.Fatalf("cfg1 item[1]: %+v", cfg1.DirectItems[1])
	}
	if cfg1.DirectItems[1].UnitPrice != 5100 {
		t.Fatalf("cfg1 item[1] price: want 5100, got %v", cfg1.DirectItems[1].UnitPrice)
	}

	cfg2 := workspace.Configurations[1]
	if cfg2.Article != "DL380-ARTICLE-2" {
		t.Fatalf("cfg2 article: want DL380-ARTICLE-2, got %q", cfg2.Article)
	}
	if cfg2.ServerCount != 1 {
		t.Fatalf("cfg2 server_count: want 1, got %d", cfg2.ServerCount)
	}
	if len(cfg2.DirectItems) != 1 || cfg2.DirectItems[0].LotName != "SSD_NVMe" {
		t.Fatalf("cfg2 items: %+v", cfg2.DirectItems)
	}
}

func TestIsQuoteForgeCSV(t *testing.T) {
	withBOM := "\xEF\xBB\xBFLine;Type;p/n;Description;Qty (1 pcs.);Qty (total);Price (1 pcs.);Price (total)\n10;;ART;;1;;100;\n"
	noBOM := "Line;Type;p/n;Description;Qty (1 pcs.);Qty (total);Price (1 pcs.);Price (total)\n"

	cases := []struct {
		input string
		want  bool
	}{
		{withBOM, true},
		{noBOM, true},
		{"<CFXML>\n</CFXML>", false},
		{"|CPU*1\n|PSU*2", false},
		{"", false},
		{"Line;other;columns\n", false},
	}
	for _, tc := range cases {
		got := IsQuoteForgeCSV([]byte(tc.input))
		if got != tc.want {
			t.Errorf("IsQuoteForgeCSV(%q) = %v, want %v", tc.input[:min(len(tc.input), 40)], got, tc.want)
		}
	}
}

func TestParseCSVPrice(t *testing.T) {
	cases := []struct {
		input string
		want  float64
	}{
		{"2074,5", 2074.5},
		{"5100", 5100},
		{"104 700", 104700},
		{"20 940", 20940},
		{"—", 0},
		{"", 0},
		{"abc", 0},
	}
	for _, tc := range cases {
		got := parseCSVPrice(tc.input)
		if got != tc.want {
			t.Errorf("parseCSVPrice(%q) = %v, want %v", tc.input, got, tc.want)
		}
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func TestIsInspurBOM(t *testing.T) {
	cases := []struct {
		input string
		want  bool
	}{
		{"|CPU_AMD*1\n|PSU*2", true},
		{"CPU_AMD*1", true},
		{"<CFXML>\n</CFXML>", false},
		{"just text\nno stars", false},
		{"pn*abc", false},
		{"", false},
	}
	for _, tc := range cases {
		got := IsInspurBOM([]byte(tc.input))
		if got != tc.want {
			t.Errorf("IsInspurBOM(%q) = %v, want %v", tc.input, got, tc.want)
		}
	}
}

func TestIsTextBOM(t *testing.T) {
	cases := []struct {
		input string
		want  bool
	}{
		{"CPU Intel 6760P - 2 шт.", true},
		{"Fan 18Krpm 8086 - 20 шт.\nRail L-Type 665mm - 1 шт.", true},
		{"NVIDIA transceiver - 8шт.", true},        // no space before шт
		{"Сервер KR9288X3, в составе:\nFan - 4 шт.", true},
		{"|CPU_AMD*1\n|PSU*2", false},               // Inspur
		{"<CFXML>\n</CFXML>", false},
		{"just text\nno quantities", false},
		{"CPU - 2 pcs.", false},                     // not Russian шт
		{"", false},
	}
	for _, tc := range cases {
		got := IsTextBOM([]byte(tc.input))
		if got != tc.want {
			t.Errorf("IsTextBOM(%q) = %v, want %v", tc.input, got, tc.want)
		}
	}
}

func TestParseTextBOM(t *testing.T) {
	const sample = `Сервер KR9288X3, в составе:
GPU-NVIDIA HGX B300 8-GPU-2304GB HBM3E - 1 шт.
incl. onboard 800G XDR - 8 шт.
CPU Intel 6760P Xeon 2.2GHz 64C 320M 330W - 2 шт.
Mem 128G DDR5-6400MHz ECC-RDIMM - 16 шт.
SSD 960G U.2 16GTps 2.5in RAID_1 - 2 шт.
SSD 3.84T U.2 16GTps 2.5in R-Standard - 2 шт.
NIC 25Gbps 2Port LC Nvidia CX6LX PCIe MM GEN4 - 1 шт.
PowerSupply 3200W Titanium 220VACor240VDC - 2 шт.
PowerSupply 3300W Titanium 220VACor240VDC - 6 шт.
PowerCord 1.9M C20 C19 - 14 шт.
Rail L-Type 665mm - 1 шт.
Chassis 2.5x12 gpu - 1 шт.
Fan 18Krpm 8086 - 20 шт.
NVIDIA twin port transceiver, 800Gbps,2xNDR, OSFP, 2xMPO12 APC, 850nm MM F, up to 50m, flat top - 8шт.`

	workspace, err := parseTextBOM([]byte(sample), "spec.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if workspace.SourceFormat != "Text" {
		t.Fatalf("expected SourceFormat Text, got %q", workspace.SourceFormat)
	}
	if len(workspace.Configurations) != 1 {
		t.Fatalf("expected 1 configuration, got %d", len(workspace.Configurations))
	}
	cfg := workspace.Configurations[0]
	if cfg.Name != "KR9288X3" {
		t.Fatalf("expected name KR9288X3 (from header), got %q", cfg.Name)
	}
	if cfg.ServerModel != "KR9288X3" {
		t.Fatalf("expected ServerModel KR9288X3, got %q", cfg.ServerModel)
	}
	if cfg.ServerCount != 1 {
		t.Fatalf("expected ServerCount 1, got %d", cfg.ServerCount)
	}
	const wantRows = 14
	if len(cfg.Rows) != wantRows {
		t.Fatalf("expected %d rows, got %d", wantRows, len(cfg.Rows))
	}

	rowsByDesc := make(map[string]localdb.VendorSpecItem, len(cfg.Rows))
	for _, r := range cfg.Rows {
		rowsByDesc[r.Description] = r
		if r.VendorPartnumber != r.Description {
			t.Fatalf("expected VendorPartnumber to mirror Description, got pn=%q desc=%q", r.VendorPartnumber, r.Description)
		}
	}

	// Description with internal hyphens and digits must not be split early.
	gpu, ok := rowsByDesc["GPU-NVIDIA HGX B300 8-GPU-2304GB HBM3E"]
	if !ok {
		t.Fatal("expected GPU row not found (check hyphen handling)")
	}
	if gpu.Quantity != 1 {
		t.Fatalf("GPU: expected qty 1, got %d", gpu.Quantity)
	}

	mem, ok := rowsByDesc["Mem 128G DDR5-6400MHz ECC-RDIMM"]
	if !ok {
		t.Fatal("expected Mem row not found")
	}
	if mem.Quantity != 16 {
		t.Fatalf("Mem: expected qty 16, got %d", mem.Quantity)
	}

	// Quantity with no space before "шт" and commas/hyphens in description.
	xcvr, ok := rowsByDesc["NVIDIA twin port transceiver, 800Gbps,2xNDR, OSFP, 2xMPO12 APC, 850nm MM F, up to 50m, flat top"]
	if !ok {
		t.Fatal("expected transceiver row not found (check no-space quantity)")
	}
	if xcvr.Quantity != 8 {
		t.Fatalf("transceiver: expected qty 8, got %d", xcvr.Quantity)
	}
}

func TestParseTextBOMVariantHeaderAndLeadingSpace(t *testing.T) {
	// Header does not start with "Сервер"; some lines have leading/trailing spaces;
	// descriptions contain commas and internal hyphens.
	const sample = `Вычислительный GPU сервер  G5500V7, в составе:
Серверное шасси G5500 V7 (12NVMe + 8SAS/SATA) - 1 шт.
Процессор Intel 8558P 48C 2.7G 260MB 350W - 2 шт.
Модуль оперативной памяти Mem 128G DDR5-5600MHz ECC-RDIMM - 16 шт.
Накопитель SSD 2.5" NVMe 3.84TB - 8 шт.
 Накопитель SSD 2.5" SATA 3.84TB - 2 шт.
Адаптер SAS/SATA RAID0,1,10 12Gb/s-no Cache - 1 шт.
Адаптер 25GE(CX6-Lx)-Dual Port SFP28 - 1 шт.
Сетевая карта 4 x 1G, Base-T - 1 шт.
Адаптер HBA Emulex LPe32002 2 Port 32GFC - 1 шт.
Крепежный комплект Ball Bearing Rail Kit - 1 шт.
Кабельный органайзер Cable Management Arm - 1 шт.
Кабель питания PowerCord 3m C20 C19 - 4 шт.
Видеокарта (видеоускоритель) NVIDIA RTX PRO 6000 Server Edition 96GB GDDR7 - 8 шт.
Блок питания 3000W Titanium AC Power Supply - 4 шт.`

	workspace, err := parseTextBOM([]byte(sample), "spec.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	cfg := workspace.Configurations[0]
	if cfg.ServerModel != "G5500V7" {
		t.Fatalf("expected ServerModel G5500V7 (last token before comma), got %q", cfg.ServerModel)
	}
	if cfg.Name != "G5500V7" {
		t.Fatalf("expected name G5500V7, got %q", cfg.Name)
	}
	const wantRows = 14
	if len(cfg.Rows) != wantRows {
		t.Fatalf("expected %d rows, got %d", wantRows, len(cfg.Rows))
	}

	for _, r := range cfg.Rows {
		if r.VendorPartnumber != strings.TrimSpace(r.VendorPartnumber) {
			t.Fatalf("vendor_partnumber has surrounding whitespace: %q", r.VendorPartnumber)
		}
		if r.Description != strings.TrimSpace(r.Description) {
			t.Fatalf("description has surrounding whitespace: %q", r.Description)
		}
		if r.VendorPartnumber == "" {
			t.Fatal("empty vendor_partnumber")
		}
	}

	rowsByDesc := make(map[string]localdb.VendorSpecItem, len(cfg.Rows))
	for _, r := range cfg.Rows {
		rowsByDesc[r.VendorPartnumber] = r
	}

	// Leading-space line must yield a trimmed P/N.
	sata, ok := rowsByDesc[`Накопитель SSD 2.5" SATA 3.84TB`]
	if !ok {
		t.Fatal("expected SATA SSD row not found (check leading-space trimming)")
	}
	if sata.Quantity != 2 {
		t.Fatalf("SATA SSD: expected qty 2, got %d", sata.Quantity)
	}

	// Commas inside the description must not break parsing.
	raid, ok := rowsByDesc["Адаптер SAS/SATA RAID0,1,10 12Gb/s-no Cache"]
	if !ok {
		t.Fatal("expected RAID adapter row not found (check commas in description)")
	}
	if raid.Quantity != 1 {
		t.Fatalf("RAID adapter: expected qty 1, got %d", raid.Quantity)
	}

	gpu, ok := rowsByDesc["Видеокарта (видеоускоритель) NVIDIA RTX PRO 6000 Server Edition 96GB GDDR7"]
	if !ok {
		t.Fatal("expected GPU row not found")
	}
	if gpu.Quantity != 8 {
		t.Fatalf("GPU: expected qty 8, got %d", gpu.Quantity)
	}
}

func TestParsePastedBOMText(t *testing.T) {
	t.Run("text BOM", func(t *testing.T) {
		rows, format := ParsePastedBOMText("Сервер X1, в составе:\nCPU Intel 6760P - 2 шт.\nMem 128G - 16 шт.")
		if format != "Text" {
			t.Fatalf("expected format Text, got %q", format)
		}
		if len(rows) != 2 {
			t.Fatalf("expected 2 rows, got %d", len(rows))
		}
		if rows[0].VendorPartnumber != "CPU Intel 6760P" || rows[0].Quantity != 2 {
			t.Fatalf("unexpected first row: %+v", rows[0])
		}
	})

	t.Run("inspur BOM", func(t *testing.T) {
		rows, format := ParsePastedBOMText("|CPU_AMD*1\n|PSU*2")
		if format != "Inspur" {
			t.Fatalf("expected format Inspur, got %q", format)
		}
		if len(rows) != 2 || rows[1].Quantity != 2 {
			t.Fatalf("unexpected rows: %+v", rows)
		}
	})

	t.Run("unrecognized falls through", func(t *testing.T) {
		rows, format := ParsePastedBOMText("col a\tcol b\nfoo\tbar")
		if rows != nil || format != "" {
			t.Fatalf("expected nil/empty for unrecognized text, got rows=%+v format=%q", rows, format)
		}
	})
}

func TestParseTextBOMNameFromFilename(t *testing.T) {
	const sample = "CPU Intel 6760P - 2 шт.\nMem 128G - 16 шт."
	workspace, err := parseTextBOM([]byte(sample), "my-config.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	cfg := workspace.Configurations[0]
	if cfg.Name != "my-config" {
		t.Fatalf("expected name my-config (from filename), got %q", cfg.Name)
	}
	if cfg.ServerModel != "" {
		t.Fatalf("expected empty ServerModel without header, got %q", cfg.ServerModel)
	}
	if len(cfg.Rows) != 2 {
		t.Fatalf("expected 2 rows, got %d", len(cfg.Rows))
	}
}
