import { describe, it, expect } from 'vitest'
import {
  calcDataQualityModifier,
  calcConfidenceModifier,
  applyHardFilters,
  calculateScore,
} from '../scoreCalculator'
import { AssetType, ResultType, AvailabilityStatus, FreshnessStatus } from '../../../domain/enums'
import { makeNeed, makeProperty } from './fixtures'

// ── calcDataQualityModifier ───────────────────────────────────────────────────

describe('calcDataQualityModifier', () => {
  function dq(score: number) {
    return makeProperty({
      dataQuality: { score, freshness: FreshnessStatus.FRESH, missingCriticalFields: [], missingOptionalFields: [], warnings: [] },
    })
  }

  it('returns +5 for excellent data quality (score >= 0.85)', () => {
    expect(calcDataQualityModifier(dq(0.90))).toBe(5)
    expect(calcDataQualityModifier(dq(0.85))).toBe(5)
  })

  it('returns 0 for good data quality (0.70 ≤ score < 0.85)', () => {
    expect(calcDataQualityModifier(dq(0.75))).toBe(0)
    expect(calcDataQualityModifier(dq(0.70))).toBe(0)
  })

  it('returns -5 for fair data quality (0.55 ≤ score < 0.70)', () => {
    expect(calcDataQualityModifier(dq(0.60))).toBe(-5)
    expect(calcDataQualityModifier(dq(0.55))).toBe(-5)
  })

  it('returns -10 for poor data quality (0.40 ≤ score < 0.55)', () => {
    expect(calcDataQualityModifier(dq(0.45))).toBe(-10)
    expect(calcDataQualityModifier(dq(0.40))).toBe(-10)
  })

  it('returns -15 for critical data quality (score < 0.40)', () => {
    expect(calcDataQualityModifier(dq(0.30))).toBe(-15)
    expect(calcDataQualityModifier(dq(0.00))).toBe(-15)
  })
})

// ── calcConfidenceModifier ────────────────────────────────────────────────────

describe('calcConfidenceModifier', () => {
  it('returns +3 for verified portfolio with high confidence (>= 0.80)', () => {
    const p = makeProperty({ resultType: ResultType.VERIFIED_PORTFOLIO, confidenceScore: 0.85 })
    expect(calcConfidenceModifier(p)).toBe(3)
  })

  it('returns 0 for verified portfolio with medium confidence (< 0.80)', () => {
    const p = makeProperty({ resultType: ResultType.VERIFIED_PORTFOLIO, confidenceScore: 0.75 })
    expect(calcConfidenceModifier(p)).toBe(0)
  })

  it('returns -2 for Maison Work results', () => {
    const p = makeProperty({ resultType: ResultType.MAISON_WORK, confidenceScore: 0.80 })
    expect(calcConfidenceModifier(p)).toBe(-2)
  })

  it('returns -15 for future availability signals', () => {
    const p = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY, confidenceScore: 0.70 })
    expect(calcConfidenceModifier(p)).toBe(-15)
  })

  it('stacks an additional -10 penalty when confidenceScore < 0.50', () => {
    // MAISON_WORK (-2) + LOW_CONFIDENCE (-10) = -12
    const p = makeProperty({ resultType: ResultType.MAISON_WORK, confidenceScore: 0.45 })
    expect(calcConfidenceModifier(p)).toBe(-12)
  })

  it('stacks -10 on FUTURE_AVAILABILITY when confidence is also low', () => {
    // FUTURE_AVAILABILITY (-15) + LOW_CONFIDENCE (-10) = -25
    const p = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY, confidenceScore: 0.40 })
    expect(calcConfidenceModifier(p)).toBe(-25)
  })
})

// ── applyHardFilters ──────────────────────────────────────────────────────────

describe('applyHardFilters', () => {
  it('passes a fully compatible property-need pair', () => {
    const result = applyHardFilters(makeNeed(), makeProperty())
    expect(result.excluded).toBe(false)
    expect(result.severePenalty).toBe(0)
  })

  it('excludes when asset types are incompatible', () => {
    const need = makeNeed({ assetType: AssetType.OFFICE })
    const prop = makeProperty({ assetType: AssetType.RETAIL })
    const result = applyHardFilters(need, prop)
    expect(result.excluded).toBe(true)
    expect(result.reason).toMatch(/Nutzungstyp/)
  })

  it('does NOT exclude MIXED asset type for any need', () => {
    const need = makeNeed({ assetType: AssetType.OFFICE })
    const prop = makeProperty({ assetType: AssetType.MIXED })
    expect(applyHardFilters(need, prop).excluded).toBe(false)
  })

  it('excludes when property area is below the 85% minimum tolerance', () => {
    // need.min = 200, tolerance threshold = 200 * 0.85 = 170. Use 150 m².
    const need = makeNeed({ requiredArea: { min: 200, max: 400 } })
    const prop = makeProperty({ areaSqm: 150 })
    const result = applyHardFilters(need, prop)
    expect(result.excluded).toBe(true)
    expect(result.reason).toMatch(/Fläche/)
  })

  it('does NOT exclude when area is at the 85% tolerance boundary', () => {
    // 170 = 200 * 0.85, NOT less-than, so should pass
    const need = makeNeed({ requiredArea: { min: 200, max: 400 } })
    const prop = makeProperty({ areaSqm: 170 })
    expect(applyHardFilters(need, prop).excluded).toBe(false)
  })

  it('excludes when rent exceeds 150% of max budget', () => {
    // maxBudget = 50, exclusion threshold = 50 * 1.50 = 75. Use 80 CHF/m².
    const need = makeNeed({ budgetRange: { maxPerSqm: 50, currency: 'CHF' } })
    const prop = makeProperty({ rentPricePerSqm: 80 })
    const result = applyHardFilters(need, prop)
    expect(result.excluded).toBe(true)
    expect(result.reason).toMatch(/Budget/)
  })

  it('does NOT exclude occupied properties but applies a 25-point severe penalty', () => {
    const prop = makeProperty({ availabilityStatus: AvailabilityStatus.OCCUPIED })
    const result = applyHardFilters(makeNeed(), prop)
    expect(result.excluded).toBe(false)
    expect(result.severePenalty).toBe(25)
  })

  it('excludes when the property city is in the excluded locations list', () => {
    const need = makeNeed({ excludedLocations: ['Zürich'] })
    const prop = makeProperty({ location: { city: 'Zürich', country: 'CH' } })
    const result = applyHardFilters(need, prop)
    expect(result.excluded).toBe(true)
    expect(result.reason).toMatch(/ausgeschlossen/)
  })
})

// ── calculateScore ────────────────────────────────────────────────────────────

describe('calculateScore', () => {
  it('scores a perfect match (right city, area, budget, available now) above 85', () => {
    const output = calculateScore(makeNeed(), makeProperty())
    expect(output.excluded).toBe(false)
    expect(output.finalScore).toBeGreaterThanOrEqual(85)
  })

  it('scores a weak match (wrong city, over budget, poor DQ, low confidence) below 50', () => {
    const need = makeNeed({ preferredLocations: ['Zürich'] })
    const prop = makeProperty({
      location: { city: 'Basel', country: 'CH' },
      rentPricePerSqm: 70,             // 140% of max budget 50 — not excluded but severe penalty
      resultType: ResultType.VERIFIED_PORTFOLIO,
      confidenceScore: 0.45,           // triggers LOW_CONFIDENCE -10 stacking
      dataQuality: {
        score: 0.30,                   // CRITICAL → -15
        freshness: FreshnessStatus.STALE,
        missingCriticalFields: [],
        missingOptionalFields: [],
        warnings: [],
      },
    })
    const output = calculateScore(need, prop)
    expect(output.excluded).toBe(false)
    expect(output.finalScore).toBeLessThan(50)
  })

  it('returns finalScore 0 and excluded=true for a hard-filtered property', () => {
    const need = makeNeed({ assetType: AssetType.OFFICE })
    const prop = makeProperty({ assetType: AssetType.RETAIL })
    const output = calculateScore(need, prop)
    expect(output.excluded).toBe(true)
    expect(output.finalScore).toBe(0)
    expect(output.excludedReason).toBeTruthy()
  })

  // ── Ausbau-Annuität im Budget-Faktor ────────────────────────────────────────
  function budgetFactor(need: ReturnType<typeof makeNeed>, prop: ReturnType<typeof makeProperty>) {
    return calculateScore(need, prop).allHardFactors.find(f => f.criterion === 'budget')!
  }

  it('tenant-borne SHELL lowers the budget factor vs a fitted-out (FULL) property', () => {
    const need = makeNeed({ budgetRange: { maxPerSqm: 50, currency: 'CHF' } })
    const base = { rentPricePerSqm: 48, areaSqm: 300 }
    const shell = makeProperty({ ...base, hardFacts: { fitOut: 'SHELL' } })
    const full = makeProperty({ ...base, hardFacts: { fitOut: 'FULL' } })
    expect(budgetFactor(need, shell).score).toBeLessThan(budgetFactor(need, full).score)
  })

  it('landlord-borne SHELL keeps the budget factor (no fit-out surcharge)', () => {
    const need = makeNeed({ budgetRange: { maxPerSqm: 50, currency: 'CHF' } })
    const base = { rentPricePerSqm: 48, areaSqm: 300 }
    const tenant = makeProperty({ ...base, hardFacts: { fitOut: 'SHELL' } })
    const landlord = makeProperty({ ...base, hardFacts: { fitOut: 'SHELL', fitOutByLandlord: true } })
    expect(budgetFactor(need, landlord).score).toBeGreaterThan(budgetFactor(need, tenant).score)
  })

  it('produces identical scores on repeated calls with the same inputs (determinism)', () => {
    const need = makeNeed()
    const prop = makeProperty()
    const first = calculateScore(need, prop)
    const second = calculateScore(need, prop)
    expect(first.finalScore).toBe(second.finalScore)
    expect(first.hardMatchScore).toBe(second.hardMatchScore)
    expect(first.softFactorScore).toBe(second.softFactorScore)
    expect(first.dataQualityModifier).toBe(second.dataQualityModifier)
    expect(first.confidenceModifier).toBe(second.confidenceModifier)
  })

  it('finalScore equals the weighted formula: hardScore*0.6 + softScore*0.4 + modifiers (clamped 0–100)', () => {
    const output = calculateScore(makeNeed(), makeProperty())
    const expected = Math.round(
      Math.min(100, Math.max(0,
        output.hardMatchScore * 0.60 +
        output.softFactorScore * 0.40 +
        output.dataQualityModifier +
        output.confidenceModifier,
      )),
    )
    // No mustHave criteria, no severePenalty in the base fixture — formula matches exactly
    expect(output.finalScore).toBe(expected)
  })

  it('excellent DQ and high confidence push finalScore above the base composite', () => {
    // With DQ +5 and confidence +3, finalScore > hardScore*0.6 + softScore*0.4
    const output = calculateScore(makeNeed(), makeProperty())
    const baseComposite = output.hardMatchScore * 0.60 + output.softFactorScore * 0.40
    expect(output.finalScore).toBeGreaterThan(Math.round(baseComposite))
  })

  it('low DQ and future availability type push finalScore below the base composite', () => {
    const prop = makeProperty({
      resultType: ResultType.FUTURE_AVAILABILITY,
      dataQuality: {
        score: 0.30,   // CRITICAL -15
        freshness: FreshnessStatus.OUTDATED,
        missingCriticalFields: [],
        missingOptionalFields: [],
        warnings: [],
      },
    })
    const output = calculateScore(makeNeed(), prop)
    const baseComposite = output.hardMatchScore * 0.60 + output.softFactorScore * 0.40
    expect(output.finalScore).toBeLessThan(Math.round(baseComposite))
  })

  it('occupied property penalty reduces finalScore by 25 compared to available counterpart', () => {
    const baseOutput = calculateScore(makeNeed(), makeProperty())
    const occupiedOutput = calculateScore(
      makeNeed(),
      makeProperty({ availabilityStatus: AvailabilityStatus.OCCUPIED }),
    )
    // severePenalty = 25, applied post-filter
    expect(baseOutput.finalScore - occupiedOutput.finalScore).toBe(25)
  })

  it('returns non-empty positiveFactors for a strong match', () => {
    const output = calculateScore(makeNeed(), makeProperty())
    expect(output.positiveFactors.length).toBeGreaterThan(0)
    expect(output.positiveFactors[0].score).toBeGreaterThanOrEqual(70)
  })

  it('provides allHardFactors with area, location, budget, timing for every match', () => {
    const output = calculateScore(makeNeed(), makeProperty())
    const criteria = output.allHardFactors.map(f => f.criterion)
    expect(criteria).toContain('area')
    expect(criteria).toContain('location')
    expect(criteria).toContain('budget')
    expect(criteria).toContain('timing')
  })
})
