"""Invented-data tests of new Materials boundaries; original SHS never runs."""
from concurrent.futures import ThreadPoolExecutor
import json
import uuid
from fastapi.testclient import TestClient
import pytest
from backend.app import create_app
from test_boundaries import app, client, write, job, quote, invoice, portal, ORIGIN


def material(c, j, **overrides):
    data = {'job_id': j['id'], 'name': 'Timber batten', 'quantity': '2.5', 'unit': 'm', 'cost_price': '12.34', 'supplier': 'Example Timber', 'notes': 'Private margin note', **overrides}
    r = write(c, '/api/materials', data)
    assert r.status_code == 200, r.text
    return r.json()


def catalogue(c, **overrides):
    r = write(c, '/api/catalogue', {'name': 'Oak edging', 'supplier': 'Example Timber', 'supplier_sku': 'DEMO-01', 'unit': 'm', 'cost_price': '12.34', 'charge_price': '19.99', **overrides})
    assert r.status_code == 200, r.text
    return r.json()


def add(c, cat, j, **overrides):
    return write(c, '/api/catalogue/' + cat['id'] + '/add-to-job', {'job_id': j['id'], 'version': cat['version'], 'quantity': '2.5', **overrides})


def edit_fields(row, **overrides):
    return {'version': row['version'], 'name': row['name'], 'quantity': row['quantity'], 'unit': row['unit'], 'cost_price': f"{row['cost_pence'] // 100}.{row['cost_pence'] % 100:02}", 'supplier': row['supplier'], 'supplier_sku': row['supplier_sku'], 'notes': row['notes'], 'status': row['status'], 'ordered_date': row['ordered_date'], 'received_date': row['received_date'], 'supplier_links': row['supplier_links'], **overrides}


def test_new_read_paths_staff_only_and_mutations_csrf(app):
    c = client(app); j = job(c); cat = catalogue(c); row = material(c, j)
    guest = TestClient(app, headers=ORIGIN); p, _ = portal(app, c, j)
    for denied in (guest, p):
        for url in ('/api/materials', '/api/catalogue', '/api/materials/history-match?name=Timber', '/api/materials/order?ids=' + row['id'], '/api/catalogue/' + cat['id'] + '/history'):
            assert denied.get(url).status_code == 401, url
        assert write(denied, '/api/materials', {'job_id': j['id'], 'name': 'Secret', 'cost_price': '1.00'}).status_code == 401
    c.headers['x-csrf-token'] = ''
    assert write(c, '/api/catalogue', {'name': 'No', 'cost_price': '1'}).status_code == 403
    assert write(c, '/api/materials/' + row['id'] + '/archive', {'version': 1}).status_code == 403
    # Materials do not leak into the exact-customer portal projection.
    portal_view = p.get('/api/portal/oak-and-vale/me')
    assert portal_view.status_code == 200
    assert 'Private margin note' not in portal_view.text and 'Timber batten' not in portal_view.text


def test_business_job_customer_and_catalogue_isolation(app):
    a = client(app); b = client(app, 1); ja = job(a); jb = job(b)
    ca = catalogue(a); ra = material(a, ja); rb = material(b, jb, cost_price='9.00')
    assert b.get('/api/catalogue').json() == []
    assert b.get('/api/materials').json()['items'][0]['id'] == rb['id']
    for url in ('/api/materials?job_id=' + ja['id'], '/api/materials?customer_id=' + ja['customer_id'], '/api/catalogue/' + ca['id'] + '/history', '/api/materials/order?ids=' + ra['id']):
        assert b.get(url).status_code == 404, url
    assert write(b, '/api/materials', {'job_id': ja['id'], 'name': 'Wrong business', 'cost_price': '1'}).status_code == 404
    assert add(b, ca, jb).status_code == 404
    assert add(a, ca, jb).status_code == 404
    assert write(b, '/api/materials/' + ra['id'], edit_fields(ra), method='put').status_code == 404
    assert write(b, '/api/materials/' + ra['id'] + '/archive', {'version': 1}).status_code == 404
    assert write(a, '/api/materials', {'job_id': ja['id'], 'name': 'Spoof', 'cost_price': '1', 'business_id': 'harbour'}).status_code == 422


def test_same_name_customers_filter_by_id_and_historical_jobs(app):
    c = client(app); a = job(c, 'Same Demo', 'same@customer.example'); b = job(c, 'Same Demo', 'same@customer.example')
    ra = material(c, a); rb = material(c, b)
    result = c.get('/api/materials?customer_id=' + a['customer_id']).json()
    assert [r['id'] for r in result['items']] == [ra['id']]
    assert result['total_pence'] == 3085
    assert write(c, '/api/jobs/' + a['id'], {'version': 1, 'status': 'completed'}, method='put').status_code == 200
    assert [r['id'] for r in c.get('/api/materials').json()['items']] == [rb['id']]
    assert len(c.get('/api/materials?historical=true').json()['items']) == 2
    assert c.get('/api/materials?job_id=' + a['id']).json()['items'][0]['id'] == ra['id']


def test_exact_costs_fractional_quantity_and_rounding(app):
    c = client(app); j = job(c)
    r = material(c, j)
    assert r['quantity'] == '2.5' and r['cost_pence'] == 1234 and r['total_pence'] == 3085
    assert material(c, j, quantity='1.5', cost_price='0.01')['total_pence'] == 2
    assert material(c, j, quantity='0.5', cost_price='0.01')['total_pence'] == 1
    assert material(c, j, quantity='0.001', cost_price='10.00')['total_pence'] == 1
    assert material(c, j, quantity='3', cost_price='0')['total_pence'] == 0


@pytest.mark.parametrize('field,value', [('quantity','0'),('quantity','-1'),('quantity','NaN'),('quantity','1.0001'),('quantity','1000001'),('quantity',True),('cost_price','-1'),('cost_price','1.001'),('cost_price','NaN'),('cost_price','Infinity'),('cost_price','1e999999999'),('cost_price','1000000.01'),('cost_price',True),('status','paid'),('name','   '),('ordered_date','2026-02-30')])
def test_invalid_material_input_writes_nothing(app, field, value):
    c = client(app); j = job(c)
    r = write(c, '/api/materials', {'job_id': j['id'], 'name': 'Test material', 'cost_price': '1.00', field: value})
    assert r.status_code in (400, 422), r.text
    assert c.get('/api/materials').json()['items'] == []


def test_supplier_references_safe_and_preserved(app):
    c = client(app); j = job(c)
    ref = {'label': 'Manual reference', 'url': 'https://supplier.example/item?q=oak', 'last_price': '12.34', 'last_checked': '2026-10-20'}
    r = material(c, j, supplier_links=[ref])
    assert r['supplier_links'] == [ref]
    for url in ('javascript:alert(1)', 'data:text/html,test', 'https://user:password@supplier.example', 'file:///etc/passwd', 'https://bad.example\\path', 'https://bad.example:wrong'):
        response = write(c, '/api/materials', {'job_id': j['id'], 'name': 'Bad ref', 'cost_price': '1', 'supplier_links': [{**ref, 'url': url}]})
        assert response.status_code == 422, response.text
    assert len(c.get('/api/materials').json()['items']) == 1


def test_stages_dates_versions_archival_and_no_reparenting(app):
    c = client(app); j = job(c); other = job(c)
    row = material(c, j, status='received')
    assert row['ordered_date'] == row['received_date'] and len(row['received_date']) == 10
    assert write(c, '/api/materials/' + row['id'], edit_fields(row, job_id=other['id']), method='put').status_code == 422
    updated = write(c, '/api/materials/' + row['id'], edit_fields(row, quantity='3'), method='put')
    assert updated.status_code == 200 and updated.json()['total_pence'] == 3702
    assert write(c, '/api/materials/' + row['id'], edit_fields(row), method='put').status_code == 409
    assert write(c, '/api/materials/' + row['id'] + '/archive', {'version': 2}).status_code == 200
    assert c.get('/api/materials').json()['items'] == []
    archived = c.get('/api/materials?archived=true').json()['items'][0]
    assert archived['version'] == 3
    assert write(c, '/api/materials/' + row['id'] + '/restore', {'version': 3}).status_code == 200
    assert c.get('/api/materials').json()['total_pence'] == 3702
    assert c.delete('/api/materials/' + row['id']).status_code == 405


def test_bulk_all_or_nothing_foreign_ids_stale_versions_and_retry(app):
    c = client(app); other = client(app, 1); a = material(c, job(c)); b = material(c, job(c)); foreign = material(other, job(other))
    data = {'items': [{'id': a['id'], 'version': 1}, {'id': foreign['id'], 'version': 1}], 'status': 'received'}
    assert write(c, '/api/materials/bulk-stage', data).status_code == 404
    assert all(r['status'] == 'to_order' for r in c.get('/api/materials').json()['items'])
    data['items'][1] = {'id': b['id'], 'version': 9}
    assert write(c, '/api/materials/bulk-stage', data).status_code == 409
    assert all(r['version'] == 1 for r in c.get('/api/materials').json()['items'])
    data['items'][1]['version'] = 1
    key = str(uuid.uuid4())
    assert write(c, '/api/materials/bulk-stage', data, key=key).status_code == 200
    assert write(c, '/api/materials/bulk-stage', data, key=key).json()['count'] == 2
    assert all(r['status'] == 'received' and r['version'] == 2 and r['ordered_date'] and r['received_date'] for r in c.get('/api/materials').json()['items'])


def test_catalogue_snapshot_reference_only_no_financial_changes(app):
    c = client(app); j = job(c); q = quote(c, j); invoice(c, q)
    before = c.get('/api/workspace').json()
    cat = catalogue(c); r = add(c, cat, j)
    assert r.status_code == 200, r.text
    row = r.json()
    assert row['total_pence'] == 3085 and row['catalogue_snapshot']['default_charge_pence'] == 1999
    updated = write(c, '/api/catalogue/' + cat['id'], {'version': 1, 'name': 'Oak edging revised', 'cost_price': '99.99', 'charge_price': '125.00'}, method='put')
    assert updated.status_code == 200
    material_row = c.get('/api/materials').json()['items'][0]
    assert material_row['name'] == 'Oak edging' and material_row['cost_pence'] == 1234
    assert material_row['catalogue_snapshot']['version'] == 1
    after = c.get('/api/workspace').json()
    assert before['quotes'] == after['quotes'] and before['invoices'] == after['invoices'] and before['stats'] == after['stats']
    assert add(c, cat, j).status_code == 409
    history = c.get('/api/catalogue/' + cat['id'] + '/history').json()
    assert [r['action'] for r in history] == ['edited', 'added_to_job', 'created']
    assert history[0]['before_value']['default_cost_pence'] == 1234


def test_catalogue_uniqueness_archive_restore_and_versions(app):
    c = client(app); cat = catalogue(c)
    assert write(c, '/api/catalogue', {'name': '  OAK EDGING  ', 'cost_price': '1'}).status_code == 409
    assert write(c, '/api/catalogue/' + cat['id'] + '/archive', {'version': 1}).status_code == 200
    assert add(c, {**cat, 'version': 2}, job(c)).status_code == 409
    duplicate = catalogue(c)
    assert write(c, '/api/catalogue/' + cat['id'] + '/restore', {'version': 2}).status_code == 409
    assert write(c, '/api/catalogue/' + duplicate['id'] + '/archive', {'version': 1}).status_code == 200
    assert write(c, '/api/catalogue/' + cat['id'] + '/restore', {'version': 2}).status_code == 200
    assert len(c.get('/api/catalogue').json()) == 1
    assert c.delete('/api/catalogue/' + cat['id']).status_code == 405
    assert catalogue(client(app, 1))['name'] == cat['name']


def test_catalogue_optional_charge_price_and_invalid_prices(app):
    c = client(app); cat = catalogue(c, charge_price='')
    assert cat['default_charge_pence'] is None
    for price in ('-1', 'NaN', '1.001', '1000000.01', True):
        r = write(c, '/api/catalogue', {'name': 'Invalid cost', 'cost_price': price})
        assert r.status_code in (400, 422)
        r = write(c, '/api/catalogue', {'name': 'Invalid charge', 'cost_price': '1.00', 'charge_price': price})
        assert r.status_code in (400, 422)


def test_catalogue_add_override_and_retry(app):
    c = client(app); cat = catalogue(c); j = job(c); key = str(uuid.uuid4())
    data = {'job_id': j['id'], 'version': 1, 'quantity': '1.25', 'cost_override': '8.00'}
    r = write(c, '/api/catalogue/' + cat['id'] + '/add-to-job', data, key=key)
    assert r.status_code == 200 and r.json()['total_pence'] == 1000
    assert write(c, '/api/catalogue/' + cat['id'] + '/add-to-job', data, key=key).json()['id'] == r.json()['id']
    assert write(c, '/api/catalogue/' + cat['id'] + '/add-to-job', {**data, 'quantity': '2'}, key=key).status_code == 409
    assert len(c.get('/api/materials').json()['items']) == 1
    assert r.json()['catalogue_snapshot']['default_cost_pence'] == 1234


def test_cost_free_order_projection_and_no_partial_foreign_export(app):
    c = client(app); j = job(c); cat = catalogue(c); a = add(c, cat, j).json(); b = material(c, j)
    r = c.get('/api/materials/order?ids=' + a['id'] + ',' + b['id'])
    assert r.status_code == 200 and len(r.json()['items']) == 2
    for row in r.json()['items']:
        assert set(row) == {'name','quantity','unit','supplier','supplier_sku','job_id','service'}
    assert 'Private margin note' not in r.text and '1999' not in r.text and 'cost' not in r.text and 'customer_name' not in r.text
    assert c.get('/api/materials/order?ids=' + a['id'] + ',' + 'f'*32).status_code == 404
    assert c.get('/api/materials/order?ids=' + a['id'] + ',' + a['id']).status_code == 422
    assert write(c, '/api/materials/' + a['id'] + '/archive', {'version': 1}).status_code == 200
    assert c.get('/api/materials/order?ids=' + a['id']).status_code == 404


def test_csv_preview_payload_atomic_import_and_retry(app):
    c = client(app); j = job(c); key = str(uuid.uuid4())
    rows = [{'name': 'Batten', 'quantity': '2.5', 'cost_price': '12.34', 'supplier': 'Example Timber'}, {'name': 'Screws', 'quantity': '1', 'cost_price': '4.50'}]
    payload = {'job_id': j['id'], 'rows': rows}
    invalid = {'job_id': j['id'], 'rows': [rows[0], {**rows[1], 'cost_price': 'NaN'}]}
    assert write(c, '/api/materials/import', invalid).status_code == 422
    assert c.get('/api/materials').json()['items'] == []
    # Later row fails during insertion (implicit received date before future order).
    invalid['rows'][1] = {**rows[1], 'status': 'received', 'ordered_date': '2999-01-01'}
    assert write(c, '/api/materials/import', invalid).status_code == 422
    assert c.get('/api/materials').json()['items'] == []
    r = write(c, '/api/materials/import', payload, key=key)
    assert r.status_code == 200 and r.json()['total_pence'] == 3535
    assert write(c, '/api/materials/import', payload, key=key).json() == r.json()
    assert len(c.get('/api/materials').json()['items']) == 2
    assert write(client(app, 1), '/api/materials/import', payload).status_code == 404
    assert write(c, '/api/materials/import', {'job_id': j['id'], 'rows': rows * 101}).status_code == 422


def test_price_history_business_scope_exact_match_and_archive(app):
    c = client(app); other = client(app, 1); j = job(c); material(c, j, name='Timber', cost_price='2')
    latest = material(c, j, name=' TIMBER ', cost_price='3')
    material(other, job(other), name='Timber', cost_price='9')
    assert c.get('/api/materials/history-match?name=timber').json()['cost_pence'] == 300
    assert c.get('/api/materials/history-match?name=Tim').json() is None
    assert other.get('/api/materials/history-match?name=timber').json()['cost_pence'] == 900
    assert write(c, '/api/materials/' + latest['id'] + '/archive', {'version': 1}).status_code == 200
    assert c.get('/api/materials/history-match?name=Timber').json()['cost_pence'] == 200


def test_concurrent_material_edits_cannot_overwrite(app):
    c = client(app); row = material(c, job(c))
    def change(qty):
        return write(c, '/api/materials/' + row['id'], edit_fields(row, quantity=qty), method='put').status_code
    with ThreadPoolExecutor(max_workers=2) as pool:
        assert sorted(pool.map(change, ('3', '4'))) == [200, 409]
    result = c.get('/api/materials').json()['items'][0]
    assert result['version'] == 2 and result['total_pence'] in (3702, 4936)


def test_additive_schema_preserves_v01_records_and_materials_restart(app):
    c = client(app); j = job(c); q = quote(c, j); root = app.state.store.root
    with app.state.store.tenant('oak') as db:
        db.execute('DROP TABLE material_audit'); db.execute('DROP TABLE job_materials'); db.execute('DROP TABLE materials_catalogue')
    reopened = create_app(root); c2 = client(reopened)
    assert c2.get('/api/workspace').json()['quotes'][0]['id'] == q['id']
    assert c2.get('/api/materials').json()['items'] == []
    row = material(c2, j)
    restarted = client(create_app(root))
    assert restarted.get('/api/materials').json()['items'][0]['id'] == row['id']
